Android利用ViewDragHelper輕松實現(xiàn)拼圖游戲的示例
前言
最近一段時間看了一些介紹ViewDragHelper的博客,感覺這是一個處理手勢滑動的神奇,看完以后就想做點東西練練手,于是就做了這個Android拼圖小游戲。
先上個效果圖
源碼 https://github.com/kevin-mob/Puzzle
ViewDragHelper
其實ViewDragHelper并不是第一個用于分析手勢處理的類,gesturedetector也是,但是在和拖動相關(guān)的手勢分析方面gesturedetector只能說是勉為其難。
關(guān)于ViewDragHelper有如下幾點:
ViewDragHelper.Callback是連接ViewDragHelper與view之間的橋梁(這個view一般是指擁子view的容器即parentView);
ViewDragHelper的實例是通過靜態(tài)工廠方法創(chuàng)建的;
你能夠指定拖動的方向;
ViewDragHelper可以檢測到是否觸及到邊緣;
ViewDragHelper并不是直接作用于要被拖動的View,而是使其控制的視圖容器中的子View可以被拖動,如果要指定某個子view的行為,需要在Callback中想辦法;
ViewDragHelper的本質(zhì)其實是分析onInterceptTouchEvent和onTouchEvent的MotionEvent參數(shù),然后根據(jù)分析的結(jié)果去改變一個容器中被拖動子View的位置( 通過offsetTopAndBottom(int offset)和offsetLeftAndRight(int offset)方法 ),他能在觸摸的時候判斷當(dāng)前拖動的是哪個子View;
雖然ViewDragHelper的實例方法 ViewDragHelper create(ViewGroup forParent, Callback cb) 可以指定一個被ViewDragHelper處理拖動事件的對象 。
實現(xiàn)思路
- 自定義PuzzleLayout繼承自RelativeLayout。
- 將PuzzleLayout的onInterceptTouchEvent和onTouchEvent交給ViewDragHelper來處理。
- 將拼圖Bitmap按九宮格切割,生成ImageView添加到PuzzleLayout并進(jìn)行排列。
- 創(chuàng)建ImageView的對應(yīng)數(shù)據(jù)模型。
- ViewDragHelper.Callback控制滑動邊界的實現(xiàn)。
- 打亂ImageView的擺放位置。
下面介紹一下以上5步的具體實現(xiàn)細(xì)節(jié)。
第一步: 創(chuàng)建一個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這個處理手勢滑動的神器。
在使用之前我們先簡單的了解一下它的相關(guān)函數(shù)。
/** * 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)
上面這個是創(chuàng)建一個ViewDragHelper的靜態(tài)函數(shù),根據(jù)注釋我們可以了解到:
- 第一個參數(shù)是當(dāng)前的ViewGroup。
- 第二個參數(shù)是檢測拖動開始的靈敏度,1.0f為正常值。
- 第三個參數(shù)Callback,是ViewDragHelper給ViewGroup的回調(diào)。
這里我們主要來看看Callback這個參數(shù),Callback會在手指觸摸當(dāng)前ViewGroup的過程中不斷返回解析到的相關(guān)事件和狀態(tài),并獲取ViewGroup返回給ViewDragHelper的狀態(tài),來決定接下來的操作是否需要執(zhí)行,從而達(dá)到了在ViewGroup中管理和控制ViewDragHelper的目的。
Callback的方法很多,這里主要介紹本文用到的幾個方法
public abstract boolean tryCaptureView(View child, int pointerId)
嘗試捕獲當(dāng)前手指觸摸到的子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)
當(dāng)手指從childView上離開時回調(diào)。
有了以上這些函數(shù),我們的拼圖游戲大致就可以做出來了,通過ViewDragHelper.create()來創(chuàng)建一個ViewDragHelper,通過Callback中tryCaptureView來控制當(dāng)前觸摸的子view是否可以滑動,clampViewPositionHorizontal、clampViewPositionVertical來控制水平方向和垂直方向的移動邊界,具體的方法實現(xiàn)會在后面講到。
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并進(jìn)行排列。
首先,外界需要傳入一個切割參數(shù)mSquareRootNum做為寬和高的切割份數(shù),我們需要獲取PuzzleLayout的寬和高,然后計算出每一塊的寬mItemWidth和高mItemHeight, 將Bitmap等比例縮放到和PuzzleLayout大小相等,然后將圖片按照類似上面這張圖所標(biāo)的形式進(jìn)行切割,生成mSquareRootNum*mSquareRootNum份Bitmap,每個Bitmap對應(yīng)創(chuàng)建一個ImageView載體添加到PuzzleLayout中,并進(jìn)行布局排列。
創(chuàng)建子view, mHelper是封裝的用來操作對應(yīng)數(shù)據(jù)模型的幫助類DataHelper。
/** * 將子View index與mHelper中models的index一一對應(yīng), * 每次在交換子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); } } }
第四步,創(chuàng)建ImageView的對應(yīng)數(shù)據(jù)模型。
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一一對應(yīng)
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控制滑動邊界的實現(xiàn)。
tryCaptureView的實現(xiàn)
public boolean tryCaptureView(View child, int pointerId) { int index = indexOfChild(child); return mHelper.getScrollDirection(index) != DataHelper.N; }
DataHelper的getScrollDirection函數(shù)
/** * 獲取索引處model的可移動方向,不能移動返回 -1。 */ int getScrollDirection(int index){ Block model = models.get(index); int position = model.position; //獲取當(dāng)前view所在位置的坐標(biāo) x y /* * * * * * * * o * * * * * * * * * * * * */ int x = position % squareRootNum; int y = position / squareRootNum; int invisibleModelPosition = models.get(0).position; /* * 判斷當(dāng)前位置是否可以移動,如果可以移動就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的實現(xiàn)細(xì)節(jié),獲取滑動方向左或右,再控制對應(yīng)的滑動區(qū)域。
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的實現(xiàn)細(xì)節(jié),獲取滑動方向上或下,再控制對應(yīng)的滑動區(qū)域。
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的實現(xiàn),當(dāng)松手時,不可見View和松開的View之間進(jìn)行布局參數(shù)交換,同時對應(yīng)的model之間也需要通過swapValueWithInvisibleModel函數(shù)進(jìn)行數(shù)據(jù)交換。
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配合實現(xiàn)松手后的動畫效果。
PuzzleLayout重寫computeScroll函數(shù)。
@Override public void computeScroll() { if(viewDragHelper.continueSettling(true)) { invalidate(); } }
swapValueWithInvisibleModel函數(shù),每次交換完成后會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的擺放位置。
這里不能隨意打亂順序,否則你可能永遠(yuǎn)也不能復(fù)原拼圖了,這里使用的辦法是每次在不可見View附近隨機(jī)找一個View與不可見View進(jìn)行位置交換,這里的位置交換指的是布局參數(shù)的交換,同時對應(yīng)的數(shù)據(jù)模型也需要進(jìn)行數(shù)據(jù)交換。
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函數(shù)
/** * 隨機(jī)查詢出不可見 * 位置周圍的一個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; }
以上為主要的代碼實現(xiàn),全部工程已上傳Github,歡迎學(xué)習(xí),歡迎star,傳送門
https://github.com/kevin-mob/Puzzle
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android 側(cè)滑關(guān)閉Activity的實例
這篇文章主要介紹了Android 側(cè)滑關(guān)閉Activity的實例的相關(guān)資料,好的手機(jī)現(xiàn)在沒有物理返回鍵,或者說統(tǒng)一Android 與IOS 軟件功能的時候,需要側(cè)滑關(guān)閉,需要的朋友可以參考下2017-07-07android與asp.net服務(wù)端共享session的方法詳解
這篇文章主要給大家介紹了關(guān)于android與asp.net服務(wù)端如何共享session的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)下吧。2017-09-09android設(shè)置adb自帶screenrecord錄屏命令
這篇文章主要介紹了android設(shè)置adb自帶screenrecord錄屏命令,需要的朋友可以參考下2018-11-11Android App的運行環(huán)境及Android系統(tǒng)架構(gòu)概覽
這篇文章主要介紹了Android App的運行環(huán)境及Android系統(tǒng)架構(gòu)概覽,并對應(yīng)用程序進(jìn)程間隔離機(jī)制等知識點作了介紹,需要的朋友可以參考下2016-03-03Android studio實現(xiàn)兩個界面間的切換
這篇文章主要為大家詳細(xì)介紹了Android studio實現(xiàn)兩個界面間的切換,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-04-04android使用DataBinding來設(shè)置空狀態(tài)
本篇文章主要介紹了android使用DataBinding來設(shè)置空狀態(tài),具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-03-03Android中實現(xiàn)開機(jī)自動啟動服務(wù)(service)實例
這篇文章主要介紹了Android中實現(xiàn)自動啟動服務(wù)實例,并開機(jī)自動啟用(無activity),的朋友可以參考下2014-06-06解決Android Studio xml 格式化不自動換行的問題
這篇文章主要介紹了解決Android Studio xml 格式化不自動換行的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03Android中AlertDialog四種對話框的最科學(xué)編寫用法(實例代碼)
這篇文章主要介紹了Android中AlertDialog四種對話框的最科學(xué)編寫用法,本文通過代碼講解的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2019-11-11