最近一段时间看了一些介绍ViewDragHelper的博客,感受这是一个处理手势滑动的神奇,看完之后就想作点东西练练手,因而就作了这个Android拼图小游戏。git
先上个效果图github
源码 https://github.com/kevin-mob/Puzzledom
下面介绍一下以上5步的具体实现细节。ide
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) { } }
这里咱们会用到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的静态函数,根据注释咱们能够了解到:布局
这里咱们主要来看看Callback这个参数,Callback会在手指触摸当前ViewGroup的过程当中不断返回解析到的相关事件和状态,并获取ViewGroup返回给ViewDragHelper的状态,来决定接下来的操做是否须要执行,从而达到了在ViewGroup中管理和控制ViewDragHelper的目的。学习
Callback的方法不少,这里主要介绍本文用到的几个方法动画
public abstract boolean tryCaptureView(View child, int pointerId)
this
尝试捕获当前手指触摸到的子view, 返回true 容许捕获,false不捕获。
public int clampViewPositionHorizontal(View child, int left, int dx)
spa
控制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; } }
首先,外界须要传入一个切割参数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); } } }
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(); } }
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; }
这里不能随意打乱顺序,不然你可能永远也不能复原拼图了,这里使用的办法是每次在不可见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