用过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中的那三个方法吧。
- tryCaptureView(View child, int pointerId)
这个方法如果返回true,说明child可以被捕获,意思就是我可以对child进行拖动
pointerId参数不是很重要,我也没理解是干嘛用的,想知道的人可以自己去看文档 - 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方向距上次移动的偏移量; - 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…
如果觉得我写得好,可以点个赞,然后关注一下的哦~另外,我的微信公众号也开播了,每天都会分享一篇优质的技术文章,欢迎大家来踩点。(公众号:代码也是人)