前言

最近一段时间看了一些介绍ViewDragHelper的博客,感觉这是一个处理手势滑动的神器,看完以后就想做点东西练练手,于是就做了这个Android拼图小游戏。

先上个效果图

demo.gif

实现思路

自定义PuzzleLayout继承自RelativeLayout。

将PuzzleLayout的onInterceptTouchEvent和onTouchEvent交给ViewDragHelper来处理。

将拼图Bitmap按九宫格切割,生成ImageView添加到PuzzleLayout并进行排列。

创建ImageView的对应数据模型。

ViewDragHelper.Callback控制滑动边界的实现。

打乱ImageView的摆放位置。

下面介绍一下以上5步的具体实现细节。

第一步: 创建一个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这个处理手势滑动的神器。

在使用之前我们先简单的了解一下它的相关函数。

/**

* 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的静态函数,根据注释我们可以了解到:

第一个参数是当前的ViewGroup。

第二个参数是检测拖动开始的灵敏度,1.0f为正常值。

第三个参数Callback,是ViewDragHelper给ViewGroup的回调。

这里我们主要来看看Callback这个参数,Callback会在手指触摸当前ViewGroup的过程中不断返回解析到的相关事件和状态,并获取ViewGroup返回给ViewDragHelper的状态,来决定接下来的操作是否需要执行,从而达到了在ViewGroup中管理和控制ViewDragHelper的目的。

Callback的方法很多,这里主要介绍本文用到的几个方法

public abstract boolean tryCaptureView(View child, int pointerId)

尝试捕获当前手指触摸到的子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)

当手指从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;

}

}

第三步,将拼图Bitmap按九宫格切割,生成ImageView添加到PuzzleLayout并进行排列。

pic1.png

首先,外界需要传入一个切割参数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);

}

}

}

第四步,创建ImageView的对应数据模型。

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 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控制滑动边界的实现。

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;

}

第六步,打乱ImageView的摆放位置。

这里不能随意打乱顺序,否则你可能永远也不能复原拼图了,这里使用的办法是每次在不可见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

android移动拼图小游戏的图片,利用ViewDragHelper轻松实现Android拼图游戏相关推荐

  1. android 分享小程序到微信,Android 分享微信小程序之图片优化

    小菜上周接入了微信分享小程序的入口,基本功能实现都没问题,有需要的朋友可以了解一下 Android 分享微信小程序失败二三事,虽然功能都正常,但整体测试发现图片展示效果不佳.于是小菜整理了一个简单的小 ...

  2. 【Android 安装包优化】WebP 图片格式兼容与性能 ( Android 中的 WebP 图片格式兼容问题 | Android 中的 WebP 图片格式性能 )

    文章目录 一.Android 中的 WebP 图片格式兼容问题 二.Android 中的 WebP 图片格式性能 三.参考资料 一.Android 中的 WebP 图片格式兼容问题 在 Android ...

  3. 就在上周,联合十多位Android技术圈的小网红,做了一份Android架构师进阶书籍推荐

    二.系统进阶书籍推荐 1.JAVA入门书籍: < Introduction to java programming > < Core java > 2.JAVA进阶书籍: &l ...

  4. android如何调取网站后台的图片,基于Bmob后台开发Android的APP项目——从后端获取图片,以及在后端上上传图片...

    第一次写博客感觉还心情还挺激动.当听说有Bmob为移动端制定的后台,正好现在我还并未学会开发后台,于是抱着尝试的心去了解一下(其实我还是个小萌新). 下面这些操作的前提已经创建好项目. 1 . 直接在 ...

  5. 汉诺塔游戏java下载,利用python实现汉诺塔游戏

    本文实例为大家分享了python实现汉诺塔游戏的具体代码,供大家参考,具体内容如下 一.汉诺塔 汉诺塔问题是一个经典的递归问题,对于这个问题,我们可以把它简单的去看成是如何用n-1去表示n. 在A,B ...

  6. 微软云游戏专利:利用服务器为平板、手机游戏加速

     2014-6-23 13:55| 发布者: xchtl| 查看: 1292| 评论: 3|来自: EXP 收藏 分享 摘要: 云游戏的概念也提出来10多年了,AMD.NVIDIA还有专门为云游戏 ...

  7. android 用代码改图,把图片转换成圆形的Android代码

    /** * 转换图片成圆形 * @param bitmap 传入Bitmap对象 * @return */ public Bitmap toRoundBitmap(Bitmap bitmap) { i ...

  8. Android调用照相机展示高清图片及展示图片时图片倾斜问题

    Android调用照相机展示高清图片及展示图片时图片倾斜问题 一.概述 二.配置权限 三.封装CameraUtil工具类 1.Android调用照相机 2.调用系统相册 3.拍摄完毕或在相册选取好照片 ...

  9. Android—拼图小游戏

    拼图小游戏 icon: 选择界面: 两个难度界面: 成功界面: 然后 这个是跟着视频教程写的 也是第一次写关于图片分割和动画的这些 大概步骤是: 1.初始化游戏主界面和方块 2.设置某个方块为缺口方块 ...

最新文章

  1. 大华嵌入式硬盘录像机数据恢复工具
  2. python人工智能——机器学习——分类算法-k近邻算法——kaggle案例: Facebook V: Predicting Check Ins
  3. mysql 根据地图 坐标 查询 周边景区、酒店
  4. 数据结构之线段树入门(单点更新区间查询)
  5. 在 React 工程中利用 Mota 编写面向对象的业务模型
  6. 百度云盘云知梦php_教你搭建私有云盘,简单快速,完全傻瓜式!不限速,永久有效!...
  7. META-INF目录是干啥用的?
  8. win10查看上次开机时间
  9. Springboot 打包成jar以后,读取resources里的文件夹
  10. 古早但有用:CISA 发布15个正遭利用的老旧漏洞
  11. 使用C#调用Java带MIME附件WebService方法的初步设想
  12. java删除文件目录及文件_Java删除文件,目录
  13. 微服务架构师的道、法、术
  14. 计算机显示u盘隐藏分区,如何查看及删除u盘的隐藏分区
  15. STOP:0x0000007E蓝屏软件故障处理
  16. 一本笔记,看懂《未来简史》!
  17. 2022帆软BI数据分析大赛他来了!
  18. mysql 绿色版的安装过程和配置
  19. Spring面向切面编程-AOP详解
  20. 西安翻译学院东区计算机教室,西安翻译学院:200间教室跨进智慧时代

热门文章

  1. ardupilot代码拉取
  2. 连续变量的全概率和贝叶斯公式_浅谈条件概率、全概率公式和贝叶斯公式
  3. 山东大学2019上半学期增强现实课程重点整理
  4. 案例:模拟京东快递单号的查询效果
  5. python时区运算,时区,时间戳,夏令时讲解
  6. LSV软件不定时无法下载谷歌影像的原因
  7. 给你入冬后的第一杯奶茶
  8. 华为应用市场APP上架流程
  9. Mac鼠标光标消失怎么办?苹果电脑鼠标指针不显示的解决方法
  10. 一个女留学生在美国的七年(转载)