话说坚持写博客是真的难,不过确实有收获,这段时间又忙成狗了,最近事不多,还是要把这个坚持下去的。
完整代码github地址:GestureViewBinder 喜欢的记得给个star呦~有什么问题或者建议也希望能够指点一下,感激不尽!
使用时在app也就是根目录下的build.gradle中添加maven仓库地址
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
然后在你项目的build.grale中添加依赖
implementation 'com.github.GeniusLiu:GestureViewBinder:v1.0.0'
最后通过一行代码使用
GestureViewBinder.bind(this, groupView, targetView);
如果你想让view充满group且大小不能小于group
gestureViewBinder.setFullGroup(true)
公司的一个项目需要给自定义控件加上缩放平移功能,前人写的代码之写了缩放…那个这个功能填坑的功能就只能交给我了。
首先,这个功能肯定需要监听用户的手势,我本来想的是直接重新控件的onTouchEvent方法的,不过前人用了一个GestureDetector
这个类及其子类和一些相关监听。这个以前确实没用到过,简单看了一下后,这是个好东西啊,缩放,平移手势全帮你写好了,还是系统自带的方法,有了这个神器,实现平移缩放还不是分分钟的事!
好了,首先我们先列出一些需求。
接下来我们就根据需求,一点一点推进我们的工具吧!
首先,最简单的就是控件的缩放。我们只需写一个全局变量
private ScaleGestureDetector scaleGestureDetector;
然后实例化这个类,里面传入系统提功能手势回调接口
scaleGestureDetector = new ScaleGestureDetector(this, new ScaleGestureDetector.OnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
Log.i("Gesture", detector.getCurrentSpan() + "---" + detector.getPreviousSpan());
Log.i("Gesture", String.valueOf(detector.getScaleFactor()));
return false;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
//这里改成true,在手势开始时拦截掉事件,交给这个接口处理
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
});
最后,再相应的控件中重写它的onTouchEvent方法(这里是监听了整个Avtivity的手势)。
@Override
public boolean onTouchEvent(MotionEvent event) {
return scaleGestureDetector.onTouchEvent(event);
}
在重写的onScale方法中打印一下detector.getScaleFactor()返回的值,我们发现,进行缩放手势的时候,这里已经能够成功的得到我们想要的缩放比例了。
如果让某个控件支持缩放手势操作,我们只需重写控件的onTouchEvent方法,然后在里面返回scaleGestureDetector的onTouchEvent方法就可以了。
这里有几点需要注意的地方
简单梳理了一下坑的地方后,我们简单的封装一下,继承ScaleGestureDetector和实现OnScaleGestureListener,把需要进行手势操作的view和对应ViewGroup当作参数传入。直接在我们的方法内部进行重写等一系列操作,这里就不多做赘述了。直接贴出伪代码。(点击事件的处理我们需要在写平移操作的时候进行优化)
//ScaleGestureListener.class
public class ScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener {
private View targetView;
private float scaleTemp = 1;
ScaleGestureListener(View targetView) {
this.targetView = targetView;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scale = detector.getScaleFactor();
scale = scaleTemp * scale;
return false;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
scaleTemp = scale;
}
}
//ScaleGestureBinder.class
public class ScaleGestureBinder extends ScaleGestureDetector {
public static ScaleGestureBinder bindView(Context context, View targetView, ViewGroup group) {
return new ScaleGestureBinder(context, targetView, group);
}
private ScaleGestureBinder(Context context, View targetView, ViewGroup group) {
super(context, new ScaleGestureListener(targetView));
group.setOnTouchListener(new View.OnTouchListener() {
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
return onTouchEvent(event);
}
});
}
}
//MainActivity.class
scaleGestureBinder = ScaleGestureBinder.bindView(this, tvTarget, rlGroup);
接下来就是根据手势回调得到的缩放比例,来对控件进行操作了。我们用view中自带的setScaleX()和setScaleY()方法就可以缩放控件了。
@Override
public boolean onScale(ScaleGestureDetector detector) {
scale = detector.getScaleFactor();
scale = scaleTemp * scale;
targetView.setScaleX(scale);
targetView.setScaleY(scale);
return false;
}
缩放写完后,就该写平移了,缩放其实非常简单,需要计算的工作几乎全在平移里,我们再归纳一下需要注意的点:
归纳好需求,我们再根据需求一点一点的实现。
首先我们先试用一下GestureDetector,写一个最简单的方法来试试这个类的基本用法。
GestureDetector里其实有好多写好的手势监听,因为需要平移,我们需要实现onScroll方法,看了一下GestureDetector里面的一些接口,我们发现可以继承SimpleOnGestureListener。
public class ScrollGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return super.onScroll(e1, e2, distanceX, distanceY);
}
@Override
public boolean onDown(MotionEvent e) {
//这里需要拦截手势,也就是将手势交给我们处理,不然有些情况会被别的事件拦截消费掉。
return true;
}
}
打印一下各个参数,我们不难发现onScroll方法里各个参数的含义。
e1为滑动前的MotionEvent,e2为滑动中或者说每一小段滑动后的MotionEvent,distanceX为X轴起始滑动坐标减去结束滑动坐标得到的距离,这种方式计算的话,向右滑为负数,向左滑为正数,这里需要注意一下,distanceY为Y轴滑动距离,特点和distanceX一样。
使用时,这个类作为参数传入GestureDetector,然后再onTouchEvent方法中,返回GestureDetector中的onTouchEvent方法即可。
gestureDetector = new GestureDetector(this, new ScrollGestureListener());
@Override
public boolean onTouchEvent(MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
知道了基本用法,我们再简单的封装一下,并且将缩放和平移的手势整合到一起。这里还有一个需要注意的点:
封装了一下,变成了这样:
//GestureViewBinder.class
private GestureViewBinder(Context context, ViewGroup viewGroup, View targetView) {
scaleGestureListener = new ScaleGestureListener(targetView,viewGroup);
scrollGestureListener = new ScrollGestureListener(targetView,viewGroup);
scaleGestureBinder = new ScaleGestureBinder(context, scaleGestureListener);
scrollGestureBinder = new ScrollGestureBinder(context, scrollGestureListener);
viewGroup.setOnTouchListener(new View.OnTouchListener() {
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("Gesture", "pointer count : " + event.getPointerCount());
if (event.getPointerCount() == 1) {
return scrollGestureBinder.onTouchEvent(event);
} else if (event.getPointerCount() == 2) {
scrollGestureListener.setScale(scaleGestureListener.getScale());
return scaleGestureBinder.onTouchEvent(event);
}
return false;
}
});
}
//ScaleGestureBinder.class
ScaleGestureBinder(Context context, ScaleGestureListener scaleGestureListener) {
super(context, scaleGestureListener);
}
//ScrollGestureBinder.class
ScrollGestureBinder(Context context, ScrollGestureListener scrollGestureListener) {
super(context, scrollGestureListener);
}
运行试了一下,缩放可以正常使用,平移也走了相应的方法,接下来就是纯计算的步骤了。
移动和缩放一样,都是从控件的原始大小开始计算的,所以我们需要保存一个全局变量来记录上次移动的距离位置。然后调用view的setTranslation方法即可。
private float distanceXTemp = 1;
private float distanceYTemp = 1;
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
distanceX = -distanceX;
distanceY = -distanceY;
distanceXTemp += distanceX;
distanceYTemp += distanceY;
targetView.setTranslationX(distanceXTemp);
targetView.setTranslationY(distanceYTemp);
return super.onScroll(e1, e2, distanceX, distanceY);
}
现在,我们的控件已经既可以滑动又可以缩放了!
接下来就是计算边界,我们先不考虑缩放的情况,只考虑控件本身可移动的大小。能够移动的距离分别为控件距左、上、右、下的距离。也就是,如果控件向左移动,则判断控件距离左边的距离是否大于0,如果大于0,才允许向左移动。以此类推上、右和下。
还有一个思路就是,判断进入这个页面时,控件距离左边的长度,因为我们存了全局的,移动总长度的变量,如果总的移动距离小于可移动的距离,则可以移动。
图画的有点水,但是能看出来,左侧最大移动距离应为getLeft,右侧为viewGrouop的宽减去getRight,上面为getTop,下面为viewGroup的高度减去getBottom。
直接上计算后的代码
//计算能移动的最大距离
maxTranslationLeft = targetView.getLeft();
maxTranslationTop = targetView.getTop();
maxTranslationRight = viewGroup.getWidth()-targetView.getRight();
maxTranslationBottom = viewGroup.getHeight()-targetView.getBottom();
distanceX = -distanceX;
distanceY = -distanceY;
//计算边界
//最大移动距离全部为正数,所以需要通过判断distanceX的正负,来判断是向左移动还是向右移动,
// 然后通过取distanceX的绝对值来和相应移动方向的最大移动距离比较
if ((distanceX < 0 && Math.abs(distanceXTemp + distanceX) < maxTranslationLeft)
|| (distanceX > 0 && distanceXTemp + distanceX < maxTranslationRight)) {
distanceXTemp += distanceX;
targetView.setTranslationX(distanceXTemp);
//如果超出边界,就移动到最大距离,防止边界有剩余量
} else if ((distanceX < 0 && Math.abs(distanceXTemp + distanceX) > maxTranslationLeft)) {
distanceXTemp = -maxTranslationLeft;
targetView.setTranslationX(-maxTranslationLeft);
} else if ((distanceX > 0 && distanceXTemp + distanceX > maxTranslationRight)) {
distanceXTemp = maxTranslationRight;
targetView.setTranslationX(maxTranslationRight);
}
if ((distanceY < 0 && Math.abs(distanceYTemp + distanceY) < maxTranslationTop)
|| (distanceY > 0 && distanceYTemp + distanceY < maxTranslationBottom)) {
distanceYTemp += distanceY;
targetView.setTranslationY(distanceYTemp);
//如果超出边界,就移动到最大距离,防止边界有剩余量
} else if ((distanceY < 0 && Math.abs(distanceYTemp + distanceY) > maxTranslationTop)) {
distanceYTemp = -maxTranslationTop;
targetView.setTranslationY(-maxTranslationTop);
} else if ((distanceY > 0 && distanceYTemp + distanceY > maxTranslationBottom)) {
distanceYTemp = maxTranslationBottom;
targetView.setTranslationY(maxTranslationBottom);
}
通过这几行代码,我们已经能够控制控件一直位于viewGroup中了。
然后是和缩放的联动。因为缩放时我们需要在ScrollGesture手势中计算可移动的距离,所以我们需要在缩放时给ScrollGesture传递一个缩放比例的参数,然后在实例化两者的时候传递。
//ScaleGestureListener.class
float getScale() {
return scale;
}
//ScrollGestureListener
void setScale(float scale) {}
//GestureViewBinder.class
viewGroup.setOnTouchListener(new View.OnTouchListener() {
scrollGestureListener.setScale(scaleGestureListener.getScale());
}
又是漫长的计算过程。分成view比group大和view比group小两种情况,每种情况又分几点需要考虑。先说一下view比group小时需要考虑的点。
view比group小的情况需要考虑两点(比group小时不需要考虑缩小,默认中心缩小即可):
1. 如果放大后view大小没有超过group边界。(view大小必定小于group)
2. 如果放大后view大小只有一面或相邻两面超过了group边界,但是总大小还是比group小(view大小必定小于group)
第一点比较简单,不用进行多余操作,只需要重新计算最大移动距离就行了。(这里只写了计算宽度的代码,高度计算是一样的。)
//放大时重新计算可移动距离
viewWidthReal = viewWidthNormal * scale;
viewHeightReal = viewHeightNormal * scale;
if (viewWidthReal < groupWidth) {
maxTranslationLeft = targetView.getLeft() - (viewWidthReal - viewWidthNormal) / 2;
maxTranslationRight = (viewGroup.getWidth() - targetView.getRight()) - (viewWidthReal - viewWidthNormal) / 2;
}
viewWidthRealTemp = viewWidthReal;
viewHeightRealTemp = viewHeightReal;
this.scale = scale;
第二点因为view仍然小于group的大小,所以我们让view超出边界时往里面移动。
//如果移动距离超过最大可移动距离(向左滑动)
if (scale > this.scale && distanceXTemp < 0 && -distanceXTemp > maxTranslationLeft) {
float translate = (viewWidthReal - viewWidthRealTemp) / 2;
targetView.setTranslationX(targetView.getTranslationX() + translate);
distanceXTemp = distanceXTemp + translate;
//如果移动距离超过最大可移动距离(向右滑动)
} else if (scale > this.scale && distanceXTemp > 0 && distanceXTemp > maxTranslationRight) {
float translate = (viewWidthReal - viewWidthRealTemp) / 2;
targetView.setTranslationX(targetView.getTranslationX() - translate);
distanceXTemp = distanceXTemp - translate;
}
目前的效果:
第二种情况是view比group大的情况,可移动距离同样需要重新计算,因为可移动距离由限制view外侧不出边界,变成了限制内侧不出边界。所以这里的计算方式有些变化。可能有点乱,给大家画张图,大家结合实际情况感受一下:
if (viewWidthReal > groupWidth) {
maxTranslationLeft = (viewWidthReal - viewWidthNormal) / 2 - (viewGroup.getWidth() - targetView.getRight());
maxTranslationRight = (viewWidthReal - viewWidthNormal) / 2 - targetView.getLeft();
}
除了必要的计算边界,这里同样需要考虑缩小时边界问题,也就是,缩小时如果一边比group小,一边比group大的情况。这种情况,我们也需要让比group小的那一边紧贴group边界。这里同样需要大家根据实际情况和后面的gif图理解一下。
if (scale < this.scale && distanceXTemp < 0 && -distanceXTemp > maxTranslationLeft) {
float translate = (viewWidthRealTemp - viewWidthReal) / 2;
targetView.setTranslationX(targetView.getTranslationX() + translate);
distanceXTemp = distanceXTemp + translate;
} else if (scale < this.scale && distanceXTemp > 0 && distanceXTemp > maxTranslationRight) {
float translate = (viewWidthRealTemp - viewWidthReal) / 2;
targetView.setTranslationX(targetView.getTranslationX() - translate);
distanceXTemp = distanceXTemp - translate;
}
因为view处理了点击事件后group就无法处理滑动缩放等手势了,所以我们把子view的点击事件屏蔽,交给group处理,这样才能避免冲突。
ScrollGestureListener正好有一个onSingleTapUp回调。我们在这里判断一下点击位置,如果点击位置在子view中,就让子view被点击。
//GestureViewBinder.class
targetView.setClickable(false);
//ScrollGestureListener.class
@Override
public boolean onSingleTapUp(MotionEvent e) {
float left = viewWidthReal > groupWidth ? 0 : (targetView.getLeft() - ((viewWidthReal - viewWidthNormal) / 2));
float top = viewHeightReal > groupHeight ? 0 : (targetView.getTop() - ((viewHeightReal - viewHeightNormal) / 2));
float right = viewWidthReal > groupWidth ? groupWidth : viewGroup.getWidth() - ((viewGroup.getWidth() - targetView.getRight()) - (viewWidthReal - viewWidthNormal) / 2);
float bottom = viewHeightReal > groupHeight ? groupHeight : viewGroup.getHeight() - ((viewGroup.getHeight() - targetView.getBottom()) - (viewHeightReal - viewHeightNormal) / 2);
RectF rectF = new RectF(left, top, right, bottom);
if (rectF.contains(e.getX(), e.getY())) {
targetView.performClick();
}
return super.onSingleTapUp(e);
}
到这里,我们的工具就告一段落了,比如我这里加了一个是否充满group的选项。大家可以自己添加一些需要的小功能。就不在赘述了。