最近一段時間看了一些介紹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