需求来源:
产品需要实现跟微信朋友圈看大图可下滑退出的效果,
但项目中不仅有大图,图中还有按钮,有文本,有视频等等,总之布局很复杂
反正这些他不管,就是要下滑退出,再缩回上一层界面小图片位置。
找了不少DEMO,基本都是只满足图片,视频实现这个功能, 那就自己来
SmartTouchLayout
多手势识别的布局,只要在布局里的VIEW,就支持:
双指、双击缩放;单指滑动;下滑退出;单击退出;不影响子控件事件;与ViewPage不冲突;
无脑调用:
FrameLayout怎么用,它就怎么用。
疑车不能无据,直接上图
双指、双击缩放;下滑退出到指定位置;
不影响子控件事件;不指定位置时,下滑到底部消失;
与ViewPage不冲突;
技术要点:
- 通过计算点击的时间差,判断是单击还是双击
private float firstClickX, firstClickY, secondClickX, secondClickY;
// 统计?ms内的点击次数
private TouchEventCountThread mInTouchEventCount = new TouchEventCountThread();
// 根据TouchEventCountThread统计到的点击次数, perform单击还是双击事件
private TouchEventHandler mTouchEventHandler = new TouchEventHandler();
private void checkClickDown(MotionEvent ev){
if (0 == mInTouchEventCount.touchCount) { // 第一次按下时,开始统计
//Log.i(TAG , "checkClickDown 第一次按下时,开始统计" );
postDelayed(mInTouchEventCount, DOUBLE_CLICK_TIME_OFFSET);
}
}
private void checkClickUp(float clickX, float clickY){
//Log.i(TAG , "checkClickUp clickX:" + clickX + ",clickY:" + clickY);
// 一次点击事件要有按下和抬起, 有抬起必有按下, 所以只需要在ACTION_UP中处理
if (!mInTouchEventCount.isLongClick) {
mInTouchEventCount.touchCount++;
if(mInTouchEventCount.touchCount == 1){
firstClickX = clickX;
firstClickY = clickY;
//Log.i(TAG , "checkClickUp 点击第一下");
}else if(mInTouchEventCount.touchCount == 2){
secondClickX = clickX;
secondClickY = clickY;
float xOff = Math.abs(firstClickX - secondClickX);
float yOff = Math.abs(firstClickY - secondClickY);
//两次点击距离相近
if(xOff < 60 && yOff < 60 ){
//Double click 成立
//Log.i(TAG , "checkClickUp Double click 成立");
}else{
//Double click 不成立,当单击处理
mInTouchEventCount.touchCount = 1;
//Log.i(TAG , "checkClickUp Double click 不成立,当单击处理");
}
}else{
mInTouchEventCount.touchCount = 0;
//Log.i(TAG , "checkClickUp 复原");
}
}else {
// 长按复原
mInTouchEventCount.isLongClick = false;
//Log.i(TAG , "checkClickUp 长按复原");
}
}
private class TouchEventCountThread implements Runnable {
public int touchCount = 0;
public boolean isLongClick = false;
@Override
public void run() {
Message msg = new Message();
if(0 == touchCount){ // long click
isLongClick = true;
} else {
msg.arg1 = touchCount;
mTouchEventHandler.sendMessage(msg);
touchCount = 0;
}
//Log.i(TAG , "TouchEventCountThread 结束:" + touchCount);
}
}
private class TouchEventHandler extends Handler {
@Override
public void handleMessage(Message msg) {
//Log.i(TAG, "touch " + msg.arg1 + " time.");
if(msg.arg1 == 1){
onSingleClicked(oldX, oldY);
}else{
onDoubleClicked(oldX, oldY);
}
}
}
-
如果单击,把事件向子VIEW传递
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction(); if(action == MotionEvent.ACTION_MOVE && mTouchState == TOUCH_MYSELF){ //Log.i(TAG, "拦截 为自己处理"); return true; } switch (action) { case MotionEvent.ACTION_DOWN: //判断单双击 checkClickDown(ev); // oldX = ev.getRawX(); oldY = ev.getRawY(); mTouchState = (isZooming || isMoving) ? TOUCH_MYSELF : TOUCH_TO_CHILDREN; //Log.e(TAG, "onInterceptTouchEvent ACTION_DOWN oldX:" + oldX + ",mTouchState:" + mTouchState); break; case MotionEvent.ACTION_MOVE: // 是否进行了滑动,设置滑动状态 float tMoveX = ev.getRawX() - oldX; final float xDiff = Math.abs(tMoveX); float tMoveY = ev.getRawY() - oldY; final float yDiff = Math.abs(tMoveY); if (yDiff > mTouchSlop || xDiff > mTouchSlop) { mTouchState = TOUCH_MYSELF; } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mTouchState = TOUCH_TO_CHILDREN; break; } // origin do return mTouchState != TOUCH_TO_CHILDREN;}
-
处理缩放
private void setScaleDetector(){ ScaleGestureDetector.OnScaleGestureListener scaleListener = new ScaleGestureDetector .SimpleOnScaleGestureListener() {
@Override public boolean onScale(ScaleGestureDetector detector) { float scaleFactor = detector.getScaleFactor(); if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) return false; //双指缩放中 isZooming = true; mCurrentScale *= scaleFactor; if(mCurrentScale < MIN_SCALE){ mCurrentScale = MIN_SCALE; } setScaleX(mCurrentScale); setScaleY(mCurrentScale); //返回 true 会一闪一闪的 return false; } @Override public void onScaleEnd(ScaleGestureDetector detector) { super.onScaleEnd(detector); //Log.i(TAG, "onScaleEnd" ); scaleEnd(); } }; mScaleDetector = new ScaleGestureDetector(getContext(), scaleListener); } -
非缩放时在onTouchEvent()中处理滑动事件
-
滑动和缩放过程中,处理边界回弹 checkBorder()
-
通过动画处理各种滑动 animXXX()
总结,关键是对手势识别和分发,代码中有详细注释,有些地方可能还是处理得不当,请大佬们指点。
如何使用
-
引用
implementation 'com.jagger:SmartTouchLayout:1.0.1' -
直接在layout.xml文件中使用,使用方式跟FrameLayout一样
<com.jagger.smartviewlibrary.SmartTouchLayout android:id="@+id/stl" android:layout_width="match_parent" android:layout_height="match_parent" > ... </com.jagger.smartviewlibrary.SmartTouchLayout> -
属性设置
设置结束时动画飞到哪去,可指定位置和大小,效果如图1;不设置,则飞到底部如图2;/** * 设置结束时,动画回到什么位置和大小 * @param w view.getWidth() 结束时的宽 * @param h view.getHeight() 结束时的高 * @param left view location[0] 结束时相对屏幕的X坐标 * @param top view location[1] 结束时相对屏幕的Y坐标 * @param scaleSide 结束时以宽/高拉伸 */ public void setEndViewLocalSize(int w, int h, int left, int top, EndViewScaleSide scaleSide) -
设置是否需要支持下滑关闭
martTouchLayout.setMoveExitEnable(true); -
设置是否需要支持缩放
smartTouchLayout.setZoomEnable(true); -
最后要把归属的Activity设置为透明
<!-- AppCompatActivity设置透明主题 --> <style name="MyTranslucentTheme" parent="Theme.AppCompat.Light.NoActionBar"> <item name="android:windowNoTitle">true</item> <item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowIsTranslucent">true</item> </style>