仿朋友圈、微博,下滑关闭大图---智能识别手势布局-SmartTouchLayout

2,311 阅读2分钟

需求来源:

产品需要实现跟微信朋友圈看大图可下滑退出的效果,
但项目中不仅有大图,图中还有按钮,有文本,有视频等等,总之布局很复杂
反正这些他不管,就是要下滑退出,再缩回上一层界面小图片位置。

找了不少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()

总结,关键是对手势识别和分发,代码中有详细注释,有些地方可能还是处理得不当,请大佬们指点。

如何使用

  1. 引用

    implementation 'com.jagger:SmartTouchLayout:1.0.1'
    
  2. 直接在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>
    
  3. 属性设置
    设置结束时动画飞到哪去,可指定位置和大小,效果如图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)
    
  4. 设置是否需要支持下滑关闭

    martTouchLayout.setMoveExitEnable(true);
    
  5. 设置是否需要支持缩放

    smartTouchLayout.setZoomEnable(true);
    
  6. 最后要把归属的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>
    

下载体验

GitHub地址:

github.com/evening424/…