前言
最近一段时间看了一些介绍ViewDragHelper的博客,感觉这是一个处理手势滑动的神奇,看完以后就想做点东西练练手,于是就做了这个Android拼图小游戏。
先上个效果图
源码 https://github.com/kevin-mob/Puzzle
实现思路
- 自定义PuzzleLayout继承自RelativeLayout。
- 将PuzzleLayout的onInterceptTouchEvent和onTouchEvent交给ViewDragHelper来处理。
- 将拼图Bitmap按九宫格切割,生成ImageView添加到PuzzleLayout并进行排列。
- 创建ImageView的对应数据模型。
- ViewDragHelper.Callback控制滑动边界的实现。
- 打乱ImageView的摆放位置。
下面介绍一下以上5步的具体实现细节。
第一步: 创建一个PuzzleLayout继承自RelativeLayout。
public class PuzzleLayout extends RelativeLayout {
public PuzzleLayout(Context context) {
super(context);
}
public PuzzleLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PuzzleLayout(Context context, AttributeSet attrs, int defStyleAttr) {
}
}
第二步:将PuzzleLayout的onInterceptTouchEvent和onTouchEvent交给ViewDragHelper来处理。
这里我们会用到ViewDragHelper这个处理手势滑动的神器。
在使用之前我们先简单的了解一下它的相关函数。
/**
* Factory method to create a new ViewDragHelper.
*
* @param forParent Parent view to monitor
* @param sensitivity Multiplier for how sensitive the helper
* should be about detecting the start of a drag.
* Larger values are more sensitive. 1.0f is normal.
* @param cb Callback to provide information and receive events
* @return a new ViewDragHelper instance
*/
public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb)
上面这个是创建一个ViewDragHelper的静态函数,根据注释我们可以了解到:
- 第一个参数是当前的ViewGroup。
- 第二个参数是检测拖动开始的灵敏度,1.0f为正常值。
- 第三个参数Callback,是ViewDragHelper给ViewGroup的回调。
这里我们主要来看看Callback这个参数,Callback会在手指触摸当前ViewGroup的过程中不断返回解析到的相关事件和状态,并获取ViewGroup返回给ViewDragHelper的状态,来决定接下来的操作是否需要执行,从而达到了在ViewGroup中管理和控制ViewDragHelper的目的。
Callback的方法很多,这里主要介绍本文用到的几个方法
-
public abstract boolean tryCaptureView(View child, int pointerId)
尝试捕获当前手指触摸到的子view, 返回true 允许捕获,false不捕获。
-
public int clampViewPositionHorizontal(View child, int left, int dx)
控制childView在水平方向的滑动,主要用来限定childView滑动的左右边界。
-
public int clampViewPositionVertical(View child, int top, int dy)
控制childView在垂直方向的滑动,主要用来限定childView滑动的上下边界。
-
public void onViewReleased(View releasedChild, float xvel, float yvel)
当手指从childView上离开时回调。
有了以上这些函数,我们的拼图游戏大致就可以做出来了,通过ViewDragHelper.create()来创建一个ViewDragHelper,通过Callback中tryCaptureView来控制当前触摸的子view是否可以滑动,clampViewPositionHorizontal、clampViewPositionVertical来控制水平方向和垂直方向的移动边界,具体的方法实现会在后面讲到。
public class PuzzleLayout extends RelativeLayout {
private ViewDragHelper viewDragHelper;
public PuzzleLayout(Context context) {
super(context);
init();
}
public PuzzleLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public PuzzleLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
mHeight = getHeight();
mWidth = getWidth();
getViewTreeObserver().removeOnPreDrawListener(this);
if(mDrawableId != 0 && mSquareRootNum != 0){
createChildren();
}
return false;
}
});
viewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return true;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
}
});
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event){
return viewDragHelper.shouldInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
viewDragHelper.processTouchEvent(event);
return true;
}
}
第三步,将拼图Bitmap按九宫格切割,生成ImageView添加到PuzzleLayout并进行排列。
首先,外界需要传入一个切割参数mSquareRootNum做为宽和高的切割份数,我们需要获取PuzzleLayout的宽和高,然后计算出每一块的宽mItemWidth和高mItemHeight, 将Bitmap等比例缩放到和PuzzleLayout大小相等,然后将图片按照类似上面这张图所标的形式进行切割,生成mSquareRootNum*mSquareRootNum份Bitmap,每个Bitmap对应创建一个ImageView载体添加到PuzzleLayout中,并进行布局排列。
创建子view, mHelper是封装的用来操作对应数据模型的帮助类DataHelper。
/**
* 将子View index与mHelper中models的index一一对应,
* 每次在交换子View位置的时候model同步更新currentPosition。
*/
private void createChildren(){
mHelper.setSquareRootNum(mSquareRootNum);
DisplayMetrics dm = getResources().getDisplayMetrics();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inDensity = dm.densityDpi;
Bitmap resource = BitmapFactory.decodeResource(getResources(), mDrawableId, options);
Bitmap bitmap = BitmapUtil.zoomImg(resource, mWidth, mHeight);
resource.recycle();
mItemWidth = mWidth / mSquareRootNum;
mItemHeight = mHeight / mSquareRootNum;
for (int i = 0; i < mSquareRootNum; i++){
for (int j = 0; j < mSquareRootNum; j++){
Log.d(TAG, "mItemWidth * x " + (mItemWidth * i));
Log.d(TAG, "mItemWidth * y " + (mItemWidth * j));
ImageView iv = new ImageView(getContext());
iv.setScaleType(ImageView.ScaleType.FIT_XY);
LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
lp.leftMargin = j * mItemWidth;
lp.topMargin = i * mItemHeight;
iv.setLayoutParams(lp);
Bitmap b = Bitmap.createBitmap(bitmap, lp.leftMargin, lp.topMargin, mItemWidth, mItemHeight);
iv.setImageBitmap(b);
addView(iv);
}
}
}
第四步,创建ImageView的对应数据模型。
public class Block {
public Block(int position, int vPosition, int hPosition){
this.position = position;
this.vPosition = vPosition;
this.hPosition = hPosition;
}
public int position;
public int vPosition;
public int hPosition;
}
DataHelper.class
子View在父类的index与mHelper中model在models的index一一对应
class DataHelper {
static final int N = -1;
static final int L = 0;
static final int T = 1;
static final int R = 2;
static final int B = 3;
private static final String TAG = DataHelper.class.getSimpleName();
private int squareRootNum;
private List<Block> models;
DataHelper(){
models = new ArrayList<>();
}
private void reset() {
models.clear();
int position = 0;
for (int i = 0; i< squareRootNum; i++){
for (int j = 0; j < squareRootNum; j++){
models.add(new Block(position, i, j));
position ++;
}
}
}
void setSquareRootNum(int squareRootNum){
this.squareRootNum = squareRootNum;
reset();
}
}
第五步,ViewDragHelper.Callback控制滑动边界的实现。
tryCaptureView的实现
public boolean tryCaptureView(View child, int pointerId) {
int index = indexOfChild(child);
return mHelper.getScrollDirection(index) != DataHelper.N;
}
DataHelper的getScrollDirection函数
/**
* 获取索引处model的可移动方向,不能移动返回 -1。
*/
int getScrollDirection(int index){
Block model = models.get(index);
int position = model.position;
//获取当前view所在位置的坐标 x y
/*
* * * * *
* * o * *
* * * * *
* * * * *
*/
int x = position % squareRootNum;
int y = position / squareRootNum;
int invisibleModelPosition = models.get(0).position;
/*
* 判断当前位置是否可以移动,如果可以移动就return可移动的方向。
*/
if(x != 0 && invisibleModelPosition == position - 1)
return L;
if(x != squareRootNum - 1 && invisibleModelPosition == position + 1)
return R;
if(y != 0 && invisibleModelPosition == position - squareRootNum)
return T;
if(y != squareRootNum - 1 && invisibleModelPosition == position + squareRootNum)
return B;
return N;
}
clampViewPositionHorizontal的实现细节,获取滑动方向左或右,再控制对应的滑动区域。
public int clampViewPositionHorizontal(View child, int left, int dx) {
int index = indexOfChild(child);
int position = mHelper.getModel(index).position;
int selfLeft = (position % mSquareRootNum) * mItemWidth;
int leftEdge = selfLeft - mItemWidth;
int rightEdge = selfLeft + mItemWidth;
int direction = mHelper.getScrollDirection(index);
//Log.d(TAG, "left " + left + " index" + index + " dx " + dx + " direction " + direction);
switch (direction){
case DataHelper.L:
if(left <= leftEdge)
return leftEdge;
else if(left >= selfLeft)
return selfLeft;
else
return left;
case DataHelper.R:
if(left >= rightEdge)
return rightEdge;
else if (left <= selfLeft)
return selfLeft;
else
return left;
default:
return selfLeft;
}
}
clampViewPositionVertical的实现细节,获取滑动方向上或下,再控制对应的滑动区域。
public int clampViewPositionVertical(View child, int top, int dy) {
int index = indexOfChild(child);
Block model = mHelper.getModel(index);
int position = model.position;
int selfTop = (position / mSquareRootNum) * mItemHeight;
int topEdge = selfTop - mItemHeight;
int bottomEdge = selfTop + mItemHeight;
int direction = mHelper.getScrollDirection(index);
//Log.d(TAG, "top " + top + " index " + index + " direction " + direction);
switch (direction){
case DataHelper.T:
if(top <= topEdge)
return topEdge;
else if (top >= selfTop)
return selfTop;
else
return top;
case DataHelper.B:
if(top >= bottomEdge)
return bottomEdge;
else if (top <= selfTop)
return selfTop;
else
return top;
default:
return selfTop;
}
}
onViewReleased的实现,当松手时,不可见View和松开的View之间进行布局参数交换,同时对应的model之间也需要通过swapValueWithInvisibleModel函数进行数据交换。
public void onViewReleased(View releasedChild, float xvel, float yvel) {
Log.d(TAG, "xvel " + xvel + " yvel " + yvel);
int index = indexOfChild(releasedChild);
boolean isCompleted = mHelper.swapValueWithInvisibleModel(index);
Block item = mHelper.getModel(index);
viewDragHelper.settleCapturedViewAt(item.hPosition * mItemWidth, item.vPosition * mItemHeight);
View invisibleView = getChildAt(0);
ViewGroup.LayoutParams layoutParams = invisibleView.getLayoutParams();
invisibleView.setLayoutParams(releasedChild.getLayoutParams());
releasedChild.setLayoutParams(layoutParams);
invalidate();
if(isCompleted){
invisibleView.setVisibility(VISIBLE);
mOnCompleteCallback.onComplete();
}
}
viewDragHelper.settleCapturedViewAt和viewDragHelper.continueSettling配合实现松手后的动画效果。
PuzzleLayout重写computeScroll函数。
@Override
public void computeScroll() {
if(viewDragHelper.continueSettling(true)) {
invalidate();
}
}
swapValueWithInvisibleModel函数,每次交换完成后会return拼图是否完成
/**
* 将索引出的model的值与不可见
* model的值互换。
*/
boolean swapValueWithInvisibleModel(int index){
Block formModel = models.get(index);
Block invisibleModel = models.get(0);
swapValue(formModel, invisibleModel);
return isCompleted();
}
/**
* 交换两个model的值
*/
private void swapValue(Block formModel, Block invisibleModel) {
int position = formModel.position;
int hPosition = formModel.hPosition;
int vPosition = formModel.vPosition;
formModel.position = invisibleModel.position;
formModel.hPosition = invisibleModel.hPosition;
formModel.vPosition = invisibleModel.vPosition;
invisibleModel.position = position;
invisibleModel.hPosition = hPosition;
invisibleModel.vPosition = vPosition;
}
/**
* 判断是否拼图完成。
*/
private boolean isCompleted(){
int num = squareRootNum * squareRootNum;
for (int i = 0; i < num; i++){
Block model = models.get(i);
if(model.position != i){
return false;
}
}
return true;
}
第六步,打乱ImageView的摆放位置。
这里不能随意打乱顺序,否则你可能永远也不能复原拼图了,这里使用的办法是每次在不可见View附近随机找一个View与不可见View进行位置交换,这里的位置交换指的是布局参数的交换,同时对应的数据模型也需要进行数据交换。
public void randomOrder(){
int num = mSquareRootNum * mSquareRootNum * 8;
View invisibleView = getChildAt(0);
View neighbor;
for (int i = 0; i < num; i ++){
int neighborPosition = mHelper.findNeighborIndexOfInvisibleModel();
ViewGroup.LayoutParams invisibleLp = invisibleView.getLayoutParams();
neighbor = getChildAt(neighborPosition);
invisibleView.setLayoutParams(neighbor.getLayoutParams());
neighbor.setLayoutParams(invisibleLp);
mHelper.swapValueWithInvisibleModel(neighborPosition);
}
invisibleView.setVisibility(INVISIBLE);
}
DataHelper中findNeighborIndexOfInvisibleModel函数
/**
* 随机查询出不可见
* 位置周围的一个model的索引。
*/
public int findNeighborIndexOfInvisibleModel() {
Block invisibleModel = models.get(0);
int position = invisibleModel.position;
int x = position % squareRootNum;
int y = position / squareRootNum;
int direction = new Random(System.nanoTime()).nextInt(4);
Log.d(TAG, "direction " + direction);
switch (direction){
case L:
if(x != 0)
return getIndexByCurrentPosition(position - 1);
case T:
if(y != 0)
return getIndexByCurrentPosition(position - squareRootNum);
case R:
if(x != squareRootNum - 1)
return getIndexByCurrentPosition(position + 1);
case B:
if(y != squareRootNum - 1)
return getIndexByCurrentPosition(position + squareRootNum);
}
return findNeighborIndexOfInvisibleModel();
}
/**
* 通过给定的位置获取model的索引
*/
private int getIndexByCurrentPosition(int currentPosition){
int num = squareRootNum * squareRootNum;
for (int i = 0; i < num; i++) {
if(models.get(i).position == currentPosition)
return i;
}
return -1;
}
以上为主要的代码实现,全部工程已上传Github,欢迎学习,欢迎star,传送门
https://github.com/kevin-mob/Puzzle
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。