美文网首页安卓资源收集Android开发Android 开发技术分享
利用ViewDragHelper轻松实现Android拼图游戏

利用ViewDragHelper轻松实现Android拼图游戏

作者: Kevinmob | 来源:发表于2017-10-31 18:06 被阅读296次

    前言

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

    先上个效果图

    demo.gif

    源码 https://github.com/kevin-mob/Puzzle

    实现思路

    1. 自定义PuzzleLayout继承自RelativeLayout。
    2. 将PuzzleLayout的onInterceptTouchEvent和onTouchEvent交给ViewDragHelper来处理。
    3. 将拼图Bitmap按九宫格切割,生成ImageView添加到PuzzleLayout并进行排列。
    4. 创建ImageView的对应数据模型。
    5. ViewDragHelper.Callback控制滑动边界的实现。
    6. 打乱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<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

    相关文章

      网友评论

      • 就叫这个算了:感谢楼主!Block类里面的vPosition和hPosition其实可以不要的吧?(就像你在其他类里面用position%count去获取)
        Kevinmob:@就叫这个算了 可以的,这里主要是为了方便使用,不必每次都去动态计算。

      本文标题:利用ViewDragHelper轻松实现Android拼图游戏

      本文链接:https://www.haomeiwen.com/subject/ruobpxtx.html