高仿 iPhone AssistiveTouch 之悬浮的小球

1,782 阅读5分钟
今天本长老带大家来讨论一下iPhone的AssistiveTouch



用过iPhone手机的同学应该都知道这个功能吧,一个半透明的悬浮的小球,官方说法就叫Assistive Touch,这个功能我个人还是比较喜欢用的,所以我就花了几天时间来研究了一下这个功能在Android手机上应该如何实现。

先介绍一下ViewDragHelper这个工具类,这个类在support.v4包中,官方提供的专门用于处理用户手指拖动的类,主要的用途也就这样吧,虽然功能但是,但是十分强大,只要几行代码,就可以搞定我们今天的这个例子。

为了增加本篇博客的神秘感,也让你们保持继续看下去的欲望,先给你们看一下效果图:



这是一个可以拖动的布局,并且可以悬浮在手机边缘,不知道为什么发不了动图,想看动图的可以去我的Github上观看,Github地址会在文末给出。当然,如果你是个热血青年,想亲自感受下这个控件的试用效果,那你可以扫描下方二维码,直接把Demo下载到手机上运行就好了。😁😁😁走过路过不要错过。强烈推荐大家下载Demo,里面还包含了很多我之前写的案例,而且我会一直更新下去。放心下载吧,里面没有鸡汤文,长老保证,都是技术干货~~~


Demo下载地址


好了,废话不多说,直接看代码吧

public class DragLayout extends RelativeLayout {

    private ViewDragHelper viewDragHelper;
    private View targetView;
    private boolean isFirstStart = false;

    public DragLayout(Context context) {
        this(context, null);
    }

    public DragLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        viewDragHelper = ViewDragHelper.create(this, 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
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (!isFirstStart) {
            synchronized (this) {
                if (!isFirstStart) {
                    isFirstStart = true;
                    targetView = getChildAt(0);
                }
            }
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return viewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        viewDragHelper.processTouchEvent(event);
        return true;
    }
}

代码很简单,无非就是继承了一个RelativeLayout,然后写了两个构造方法,构造方法中对ViewDragHelper进行了初始化。说一说ViewDragHelper中的那三个方法吧。

  1. tryCaptureView(View child, int pointerId)
    这个方法如果返回true,说明child可以被捕获,意思就是我可以对child进行拖动
    pointerId参数不是很重要,我也没理解是干嘛用的,想知道的人可以自己去看文档
  2. clampViewPositionHorizontal(View child, int left, int dx)
    关于这个方法,我举个例子吧。比如你要从原地往左边走100米,然后假设你的步长是1米。
    当你跨出去第一步的时候,这个方法会给你一个返回值left= 1(米),dx=1(米);
    当你跨出去第二步的时候,这个方法会给你一个返回值left= 2(米),dx=1(米);
    当你跨出去第三步的时候,这个方法会给你一个返回值left= 3(米),dx=1(米);
    依此类推......
    对应到程序中其实就是,当你把手指放在View上,向左边滑动的时候,left会返回该View左上角的x坐标,dx会返回x方向距上次移动的偏移量;
  3. clampViewPositionVertical(View child, int left, int dx)这个我就不解释了,道理都一样
    既然涉及到拖动,那肯定少不了onInterceptTouchEvent( )和onTouchEvent( )方法,学过事件分发机制的同学应该能听懂,听不懂的同学也没关系,直接照着我的代码抄一下吧,无可厚非。
    第一阶段就这么简单,然后看我怎么用这个控件
    
     
    
    用DragLayout包裹住控件就好了,就是这么简单,都不需要去Activity写代码
    最后运行一下,你会发现,TextView可以随意拖动。心动了吗?赶紧试一下吧,你不会后悔的,真的很简单!!!

做成这样还是远远不够的,我们应该知道,iPhone的那个半透明小圆点AssistiveTouch是可以自动回到屏幕边缘的,那我们也应该做到这样的效果才行。正巧,ViewDragHelper也给我们提供了一个方法让View回到某一个指定的位置。那现在思路就很清晰了,只要判断当我们的手指松开的时候,去执行那个方法不就好了吗。又他妈巧了,ViewDragHelper里面正好有一个回调方法就是用来判断手指松开的。来,看一下我是怎么运用这两个方法的。

public class DragLayout extends RelativeLayout {

    private ViewDragHelper viewDragHelper;
    private View targetView;
    private boolean isFirstStart = false;

    public DragLayout(Context context) {
        this(context, null);
    }

    public DragLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        viewDragHelper = ViewDragHelper.create(this, 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 child, float xvel, float yvel) {
                int top;
                int parentCenterX = getWidth() / 2;
                int childCenterX = (child.getRight() + child.getLeft()) / 2;
                if (child.getTop() < 0) {
                    top = 0;
                } else if (child.getBottom() > getBottom()) {
                    top = getBottom() - child.getHeight();
                } else {
                    top = child.getTop();
                }
                if (childCenterX < parentCenterX) {
                    viewDragHelper.settleCapturedViewAt((int) (0 - child.getWidth() * offset), top);
                } else {
                    viewDragHelper.settleCapturedViewAt((int) (getWidth() - child.getWidth() + child.getWidth() * offset), top);
                }
                invalidate();
            }

            @Override
            public int getViewVerticalDragRange(View child) {
                return getHeight();
            }
        });
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (!isFirstStart) {
            synchronized (this) {
                if (!isFirstStart) {
                    isFirstStart = true;
                    targetView = getChildAt(0);
                }
            }
        }
    }

    @Override
    public void computeScroll() {
        viewDragHelper.continueSettling(true);
        invalidate();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return viewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        viewDragHelper.processTouchEvent(event);
        return true;
    }
}

onViewReleased(View child, float xvel, float yvel)
当用户手指松开的时候,该方法会被调用,xvel是x方向的速度,yvel是y方向的速度
我在这个方法体里面主要判断了一下child到底应该往哪边靠拢,计算好了之后调用viewDragHelper.settleCapturedViewAt( )方法,然后invalidate一下,child就自动恢复到你想要的位置了。值得一提的是,在调用invalidate之后,这个控件会不断地去computeScroll,然后我们还需要做的事就是在computeScroll() 方法体里面写两句话,我上面都有了。至于为啥是这两句话,我还在找原因,你们先照抄吧,等我研究出来了再更新一下这篇博客。

最后呢,你们还是需要去自己实践一下的,把代码写出来,然后简单分析一下原因。实在懒的人可以去我的Github上拷贝一下代码,这篇博客的代码不是很完整,重点讲解思路,完整的代码在这里:github.com/Elder-Wu/No…
如果觉得我写得好,可以点个赞,然后关注一下的哦~另外,我的微信公众号也开播了,每天都会分享一篇优质的技术文章,欢迎大家来踩点。(公众号:代码也是人)