Android利用ViewDragHelper轻松实现拼图游戏的示例
作者:Kevinmob 发布时间:2022-07-10 08:57:06
前言
最近一段时间看了一些介绍ViewDragHelper的博客,感觉这是一个处理手势滑动的神奇,看完以后就想做点东西练练手,于是就做了这个Android拼图小游戏。
先上个效果图
源码 https://github.com/kevin-mob/Puzzle
ViewDragHelper
其实ViewDragHelper并不是第一个用于分析手势处理的类,gesturedetector也是,但是在和拖动相关的手势分析方面gesturedetector只能说是勉为其难。
关于ViewDragHelper有如下几点:
ViewDragHelper.Callback是连接ViewDragHelper与view之间的桥梁(这个view一般是指拥子view的容器即parentView);
ViewDragHelper的实例是通过静态工厂方法创建的;
你能够指定拖动的方向;
ViewDragHelper可以检测到是否触及到边缘;
ViewDragHelper并不是直接作用于要被拖动的View,而是使其控制的视图容器中的子View可以被拖动,如果要指定某个子view的行为,需要在Callback中想办法;
ViewDragHelper的本质其实是分析onInterceptTouchEvent和onTouchEvent的MotionEvent参数,然后根据分析的结果去改变一个容器中被拖动子View的位置( 通过offsetTopAndBottom(int offset)和offsetLeftAndRight(int offset)方法 ),他能在触摸的时候判断当前拖动的是哪个子View;
虽然ViewDragHelper的实例方法 ViewDragHelper create(ViewGroup forParent, Callback cb) 可以指定一个被ViewDragHelper处理拖动事件的对象 。
实现思路
自定义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并进行排列。
首先,外界需要传入一个切割参数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<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控制滑动边界的实现。
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
来源:http://www.jianshu.com/p/8613c748aaaf


猜你喜欢
- 一、什么是JSONJSON(JavaScript Object Notation)是一种基于JavaScript语法子集的开放标准数据交换格
- using System;using System.Collections.Generic;namespace Demo{ &nb
- 现在软件或者网页的并发量越来越大了,大量请求直接操作数据库会对数据库造成很大的压力,处理大量连接和请求就会需要很长时间,但是实际中百分之80
- 今天遇到一个问题,原来用的好好的asp网页无法打开,同时inetinfo.exe的CPU占用率很高,几乎达到了100%。看了一下系统错误日志
- Android数据存储几种方式SharePreferences数据库SQLite存储卡的文件操作Application一、共享参数Share
- 一般很多项目不是在springcloud的环境中使用的,但是需要用到分布式配置中心来管理一些外部或者项目的配置,这个时候我们可以使用spri
- 1. 对图片本身进行操作尽量不要使用 setImageBitmap、setImageResource、 BitmapFactory.deco
- 1 什么是cookie浏览器与WEB服务器之间是使用HTTP协议进行通信的,当某个用户发出页面请求时,WEB服务器只是简单的进行响应,然后就
- 如有错误,望指正;SpringBoot可以有三种方式定义初始化器,来为容器中增加自定义的对象,具体如下:1、定义在spring.factor
- 原理拓展函数是kotlin里一个比较常用的特性,例如我们可以给Context拓展一个toast方法:// MainActivity.ktfu
- 这个功能,大家也都可以去百度以下,千篇一律都自己写的(抄的)封装好的公共类,此处还是得膜拜下原创的大佬,可以花时间去搞这个,我看着都头皮发麻
- 最近有朋友问屏幕锁定的问题,自己也在学习,网上找了下也没太详细的例子,看的资料书上也没有有关屏幕锁定程序的介绍,下个小决心,自己照着官方文档
- 本篇文章介绍SpringBoot的上传和下载功能。一、创建SpringBoot工程,添加依赖compile("org.spring
- 前言对于字符串的操作,我们常用的就是trim()去除前后空格、subString()截取子字符串,其他的用的不多。下表中是字符串常用的方法。
- 公司的服务器需要实时监控,而且当用户空间已经满了,操作失败,或者出现程序Exception的时候就需要实时提醒,便于网管和程序员调式,这样就
- 背景:写一个用户登录拦截,在网上找了一圈没找到好用的,于是自己试验了一下,总结出来,分享给大家。1.自定义登录 * LoginInterce
- 1 需求Mybatis-plus使用@TableLogic注解进行逻辑删除数据后,在某些场景下,又需要查询该数据时,又不想写SQ
- 目录前言令牌中继令牌难道不能在Feign自动中继吗?实现令牌中继InheritableThreadLocal实现令牌中继总结前言在Sprin
- Java 表格数据导入word文档中个人觉得这个功能实在搞笑,没什么意义,没办法提了需求就要实现,(太好说话了把我)我的实现是再word中生
- 前言Stream是一个来自数据源的元素队列并支持聚合操作,其中具有以下特性:Stream只负责计算,不存储任何元素,元素是特定类型的对象,形