Android利用ViewDragHelper輕松實(shí)現(xiàn)拼圖游戲的示例
前言
最近一段時(shí)間看了一些介紹ViewDragHelper的博客,感覺這是一個(gè)處理手勢滑動(dòng)的神奇,看完以后就想做點(diǎn)東西練練手,于是就做了這個(gè)Android拼圖小游戲。
先上個(gè)效果圖

源碼 https://github.com/kevin-mob/Puzzle
ViewDragHelper
其實(shí)ViewDragHelper并不是第一個(gè)用于分析手勢處理的類,gesturedetector也是,但是在和拖動(dòng)相關(guān)的手勢分析方面gesturedetector只能說是勉為其難。
關(guān)于ViewDragHelper有如下幾點(diǎn):
ViewDragHelper.Callback是連接ViewDragHelper與view之間的橋梁(這個(gè)view一般是指擁子view的容器即parentView);
ViewDragHelper的實(shí)例是通過靜態(tài)工廠方法創(chuàng)建的;
你能夠指定拖動(dòng)的方向;
ViewDragHelper可以檢測到是否觸及到邊緣;
ViewDragHelper并不是直接作用于要被拖動(dòng)的View,而是使其控制的視圖容器中的子View可以被拖動(dòng),如果要指定某個(gè)子view的行為,需要在Callback中想辦法;
ViewDragHelper的本質(zhì)其實(shí)是分析onInterceptTouchEvent和onTouchEvent的MotionEvent參數(shù),然后根據(jù)分析的結(jié)果去改變一個(gè)容器中被拖動(dòng)子View的位置( 通過offsetTopAndBottom(int offset)和offsetLeftAndRight(int offset)方法 ),他能在觸摸的時(shí)候判斷當(dāng)前拖動(dòng)的是哪個(gè)子View;
雖然ViewDragHelper的實(shí)例方法 ViewDragHelper create(ViewGroup forParent, Callback cb) 可以指定一個(gè)被ViewDragHelper處理拖動(dòng)事件的對(duì)象 。
實(shí)現(xiàn)思路
- 自定義PuzzleLayout繼承自RelativeLayout。
- 將PuzzleLayout的onInterceptTouchEvent和onTouchEvent交給ViewDragHelper來處理。
- 將拼圖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來處理。
這里我們會(huì)用到ViewDragHelper這個(gè)處理手勢滑動(dòng)的神器。
在使用之前我們先簡單的了解一下它的相關(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ù)是檢測拖動(dòng)開始的靈敏度,1.0f為正常值。
- 第三個(gè)參數(shù)Callback,是ViewDragHelper給ViewGroup的回調(diào)。
這里我們主要來看看Callback這個(gè)參數(shù),Callback會(huì)在手指觸摸當(dāng)前ViewGroup的過程中不斷返回解析到的相關(guān)事件和狀態(tài),并獲取ViewGroup返回給ViewDragHelper的狀態(tà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),主要用來限定childView滑動(dòng)的左右邊界。
public int clampViewPositionVertical(View child, int top, int dy)
控制childView在垂直方向的滑動(dòng),主要用來限定childView滑動(dòng)的上下邊界。
public void onViewReleased(View releasedChild, float xvel, float yvel)
當(dāng)手指從childView上離開時(shí)回調(diào)。
有了以上這些函數(shù),我們的拼圖游戲大致就可以做出來了,通過ViewDragHelper.create()來創(chuàng)建一個(gè)ViewDragHelper,通過Callback中tryCaptureView來控制當(dāng)前觸摸的子view是否可以滑動(dòng),clampViewPositionHorizontal、clampViewPositionVertical來控制水平方向和垂直方向的移動(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是封裝的用來操作對(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<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控制滑動(dòng)邊界的實(shí)現(xiàn)。
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í),不可見View和松開的View之間進(jìn)行布局參數(shù)交換,同時(shí)對(duì)應(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配合實(shí)現(xiàn)松手后的動(dòng)畫效果。
PuzzleLayout重寫computeScroll函數(shù)。
@Override
public void computeScroll() {
if(viewDragHelper.continueSettling(true)) {
invalidate();
}
}
swapValueWithInvisibleModel函數(shù),每次交換完成后會(huì)return拼圖是否完成
/**
* 將索引出的model的值與不可見
* 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ù)原拼圖了,這里使用的辦法是每次在不可見View附近隨機(jī)找一個(gè)View與不可見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ī)查詢出不可見
* 位置周圍的一個(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();
}
/**
* 通過給定的位置獲取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
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Android實(shí)現(xiàn)拼圖小游戲
- 基于Android平臺(tái)實(shí)現(xiàn)拼圖小游戲
- Android實(shí)現(xiàn)美女拼圖游戲詳解
- Android實(shí)現(xiàn)九宮格拼圖游戲
- Android自定義View實(shí)現(xiàn)拼圖小游戲
- Android拼圖游戲 玩轉(zhuǎn)從基礎(chǔ)到應(yīng)用手勢變化
- Android實(shí)現(xiàn)滑塊拼圖驗(yàn)證碼功能
- Android 簡單的實(shí)現(xiàn)滑塊拼圖驗(yàn)證碼功能
- Android Studio做超好玩的拼圖游戲 附送詳細(xì)注釋源碼
- Android實(shí)現(xiàn)九格智能拼圖算法
相關(guān)文章
Android 側(cè)滑關(guān)閉Activity的實(shí)例
這篇文章主要介紹了Android 側(cè)滑關(guān)閉Activity的實(shí)例的相關(guān)資料,好的手機(jī)現(xiàn)在沒有物理返回鍵,或者說統(tǒng)一Android 與IOS 軟件功能的時(shí)候,需要側(cè)滑關(guān)閉,需要的朋友可以參考下2017-07-07
android與asp.net服務(wù)端共享session的方法詳解
這篇文章主要給大家介紹了關(guān)于android與asp.net服務(wù)端如何共享session的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)下吧。2017-09-09
android設(shè)置adb自帶screenrecord錄屏命令
這篇文章主要介紹了android設(shè)置adb自帶screenrecord錄屏命令,需要的朋友可以參考下2018-11-11
Android App的運(yùn)行環(huán)境及Android系統(tǒng)架構(gòu)概覽
這篇文章主要介紹了Android App的運(yùn)行環(huán)境及Android系統(tǒng)架構(gòu)概覽,并對(duì)應(yīng)用程序進(jìn)程間隔離機(jī)制等知識(shí)點(diǎn)作了介紹,需要的朋友可以參考下2016-03-03
Android studio實(shí)現(xiàn)兩個(gè)界面間的切換
這篇文章主要為大家詳細(xì)介紹了Android studio實(shí)現(xiàn)兩個(gè)界面間的切換,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04
android使用DataBinding來設(shè)置空狀態(tài)
本篇文章主要介紹了android使用DataBinding來設(shè)置空狀態(tài),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-03-03
Android中實(shí)現(xiàn)開機(jī)自動(dòng)啟動(dòng)服務(wù)(service)實(shí)例
這篇文章主要介紹了Android中實(shí)現(xiàn)自動(dòng)啟動(dòng)服務(wù)實(shí)例,并開機(jī)自動(dòng)啟用(無activity),的朋友可以參考下2014-06-06
解決Android Studio xml 格式化不自動(dòng)換行的問題
這篇文章主要介紹了解決Android Studio xml 格式化不自動(dòng)換行的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-03-03
Android中AlertDialog四種對(duì)話框的最科學(xué)編寫用法(實(shí)例代碼)
這篇文章主要介紹了Android中AlertDialog四種對(duì)話框的最科學(xué)編寫用法,本文通過代碼講解的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-11-11

