摘要:將拼圖按九宮格切割,生成添加到并進(jìn)行排列??刂苹瑒?dòng)邊界的實(shí)現(xiàn)??刂圃谒椒较虻幕瑒?dòng),主要用來(lái)限定滑動(dòng)的左右邊界。和配合實(shí)現(xiàn)松手后的動(dòng)畫效果。交換兩個(gè)的值判斷是否拼圖完成。
前言
最近一段時(shí)間看了一些介紹ViewDragHelper的博客,感覺(jué)這是一個(gè)處理手勢(shì)滑動(dòng)的神奇,看完以后就想做點(diǎn)東西練練手,于是就做了這個(gè)Android拼圖小游戲。
先上個(gè)效果圖
源碼 https://github.com/kevin-mob/Puzzle
實(shí)現(xiàn)思路自定義PuzzleLayout繼承自RelativeLayout。
將PuzzleLayout的onInterceptTouchEvent和onTouchEvent交給ViewDragHelper來(lái)處理。
將拼圖Bitmap按九宮格切割,生成ImageView添加到PuzzleLayout并進(jìn)行排列。
創(chuàng)建ImageView的對(duì)應(yīng)數(shù)據(jù)模型。
ViewDragHelper.Callback控制滑動(dòng)邊界的實(shí)現(xiàn)。
打亂ImageView的擺放位置。
下面介紹一下以上5步的具體實(shí)現(xiàn)細(xì)節(jié)。
第一步: 創(chuàng)建一個(gè)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來(lái)處理。
這里我們會(huì)用到ViewDragHelper這個(gè)處理手勢(shì)滑動(dòng)的神器。
在使用之前我們先簡(jiǎn)單的了解一下它的相關(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)
上面這個(gè)是創(chuàng)建一個(gè)ViewDragHelper的靜態(tài)函數(shù),根據(jù)注釋我們可以了解到:
第一個(gè)參數(shù)是當(dāng)前的ViewGroup。
第二個(gè)參數(shù)是檢測(cè)拖動(dòng)開(kāi)始的靈敏度,1.0f為正常值。
第三個(gè)參數(shù)Callback,是ViewDragHelper給ViewGroup的回調(diào)。
這里我們主要來(lái)看看Callback這個(gè)參數(shù),Callback會(huì)在手指觸摸當(dāng)前ViewGroup的過(guò)程中不斷返回解析到的相關(guān)事件和狀態(tài),并獲取ViewGroup返回給ViewDragHelper的狀態(tài),來(lái)決定接下來(lái)的操作是否需要執(zhí)行,從而達(dá)到了在ViewGroup中管理和控制ViewDragHelper的目的。
Callback的方法很多,這里主要介紹本文用到的幾個(gè)方法
public abstract boolean tryCaptureView(View child, int pointerId)
嘗試捕獲當(dāng)前手指觸摸到的子view, 返回true 允許捕獲,false不捕獲。
public int clampViewPositionHorizontal(View child, int left, int dx)
控制childView在水平方向的滑動(dòng),主要用來(lái)限定childView滑動(dòng)的左右邊界。
public int clampViewPositionVertical(View child, int top, int dy)
控制childView在垂直方向的滑動(dòng),主要用來(lái)限定childView滑動(dòng)的上下邊界。
public void onViewReleased(View releasedChild, float xvel, float yvel)
當(dāng)手指從childView上離開(kāi)時(shí)回調(diào)。
有了以上這些函數(shù),我們的拼圖游戲大致就可以做出來(lái)了,通過(guò)ViewDragHelper.create()來(lái)創(chuàng)建一個(gè)ViewDragHelper,通過(guò)Callback中tryCaptureView來(lái)控制當(dāng)前觸摸的子view是否可以滑動(dòng),clampViewPositionHorizontal、clampViewPositionVertical來(lái)控制水平方向和垂直方向的移動(dòng)邊界,具體的方法實(shí)現(xiàn)會(huì)在后面講到。
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)行排列。
首先,外界需要傳入一個(gè)切割參數(shù)mSquareRootNum做為寬和高的切割份數(shù),我們需要獲取PuzzleLayout的寬和高,然后計(jì)算出每一塊的寬mItemWidth和高mItemHeight, 將Bitmap等比例縮放到和PuzzleLayout大小相等,然后將圖片按照類似上面這張圖所標(biāo)的形式進(jìn)行切割,生成mSquareRootNum*mSquareRootNum份Bitmap,每個(gè)Bitmap對(duì)應(yīng)創(chuàng)建一個(gè)ImageView載體添加到PuzzleLayout中,并進(jìn)行布局排列。
創(chuàng)建子view, mHelper是封裝的用來(lái)操作對(duì)應(yīng)數(shù)據(jù)模型的幫助類DataHelper。
/** * 將子View index與mHelper中models的index一一對(duì)應(yīng), * 每次在交換子View位置的時(shí)候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的對(duì)應(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一一對(duì)應(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第五步,ViewDragHelper.Callback控制滑動(dòng)邊界的實(shí)現(xiàn)。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的實(shí)現(xiàn)
public boolean tryCaptureView(View child, int pointerId) { int index = indexOfChild(child); return mHelper.getScrollDirection(index) != DataHelper.N; }
DataHelper的getScrollDirection函數(shù)
/** * 獲取索引處model的可移動(dòng)方向,不能移動(dòng)返回 -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)前位置是否可以移動(dòng),如果可以移動(dòng)就return可移動(dòng)的方向。 */ 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的實(shí)現(xiàn)細(xì)節(jié),獲取滑動(dòng)方向左或右,再控制對(duì)應(yīng)的滑動(dò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的實(shí)現(xiàn)細(xì)節(jié),獲取滑動(dòng)方向上或下,再控制對(duì)應(yīng)的滑動(dò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的實(shí)現(xiàn),當(dāng)松手時(shí),不可見(jiàn)View和松開(kāi)的View之間進(jìn)行布局參數(shù)交換,同時(shí)對(duì)應(yīng)的model之間也需要通過(guò)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配合實(shí)現(xiàn)松手后的動(dòng)畫效果。
PuzzleLayout重寫computeScroll函數(shù)。
@Override public void computeScroll() { if(viewDragHelper.continueSettling(true)) { invalidate(); } }
swapValueWithInvisibleModel函數(shù),每次交換完成后會(huì)return拼圖是否完成
/** * 將索引出的model的值與不可見(jiàn) * model的值互換。 */ boolean swapValueWithInvisibleModel(int index){ Block formModel = models.get(index); Block invisibleModel = models.get(0); swapValue(formModel, invisibleModel); return isCompleted(); } /** * 交換兩個(gè)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ù)原拼圖了,這里使用的辦法是每次在不可見(jiàn)View附近隨機(jī)找一個(gè)View與不可見(jiàn)View進(jìn)行位置交換,這里的位置交換指的是布局參數(shù)的交換,同時(shí)對(duì)應(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ī)查詢出不可見(jiàn) * 位置周圍的一個(gè)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(); } /** * 通過(guò)給定的位置獲取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; }
以上為主要的代碼實(shí)現(xiàn),全部工程已上傳Github,歡迎學(xué)習(xí),歡迎star,傳送門
https://github.com/kevin-mob/Puzzle
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/67920.html
摘要:最近公司剛好有個(gè)活動(dòng)是要做一版的拼圖小游戲,于是自己心血來(lái)潮,自己先實(shí)現(xiàn)了一把,也算是嘗嘗鮮了。下面就把大體的思路介紹一下,希望大家都可以做出一款屬于自己的拼圖小游戲,必須是更炫酷,更好玩來(lái)吧,大家一起加油。。。 最近公司剛好有個(gè)活動(dòng)是要做一版 html5的拼圖小游戲,于是自己心血來(lái)潮,自己先實(shí)現(xiàn)了一把,也算是嘗嘗鮮了。下面就把大體的思路介紹一下,希望大家都可以做出一款屬于自己的拼圖小...
摘要:最近公司剛好有個(gè)活動(dòng)是要做一版的拼圖小游戲,于是自己心血來(lái)潮,自己先實(shí)現(xiàn)了一把,也算是嘗嘗鮮了。下面就把大體的思路介紹一下,希望大家都可以做出一款屬于自己的拼圖小游戲,必須是更炫酷,更好玩來(lái)吧,大家一起加油。。。 最近公司剛好有個(gè)活動(dòng)是要做一版 html5的拼圖小游戲,于是自己心血來(lái)潮,自己先實(shí)現(xiàn)了一把,也算是嘗嘗鮮了。下面就把大體的思路介紹一下,希望大家都可以做出一款屬于自己的拼圖小...
摘要:最近公司剛好有個(gè)活動(dòng)是要做一版的拼圖小游戲,于是自己心血來(lái)潮,自己先實(shí)現(xiàn)了一把,也算是嘗嘗鮮了。下面就把大體的思路介紹一下,希望大家都可以做出一款屬于自己的拼圖小游戲,必須是更炫酷,更好玩來(lái)吧,大家一起加油。。。 最近公司剛好有個(gè)活動(dòng)是要做一版 html5的拼圖小游戲,于是自己心血來(lái)潮,自己先實(shí)現(xiàn)了一把,也算是嘗嘗鮮了。下面就把大體的思路介紹一下,希望大家都可以做出一款屬于自己的拼圖小...
閱讀 2517·2021-11-18 10:02
閱讀 749·2021-10-08 10:04
閱讀 2390·2021-09-03 10:51
閱讀 3625·2019-08-30 15:44
閱讀 2883·2019-08-29 14:09
閱讀 2535·2019-08-29 12:21
閱讀 2130·2019-08-26 13:45
閱讀 1879·2019-08-26 13:25