Android 自定义View----触摸反馈

Android 自定义View----触摸反馈_第1张图片

布局,绘制,触摸反馈;想要实现和用户手势交互,必须了解触摸反馈,简述下:

dispatchTouchEvent:分发事件
onInterceptTouchEvent:拦截事件
onTouchEvent:消费事件

AAA------dispatchTouchEvent
AAA------onInterceptTouchEvent
BBB------dispatchTouchEvent
BBB------onInterceptTouchEvent
CCC------dispatchTouchEvent
CCC------onTouchEvent
BBB------onTouchEvent
AAA------onTouchEvent

事件从最上面的父view开始到最后一个子view,递归dispatchTouchEvent()然后调用onInterceptTouchEvent()检查自己是否拦截事件,如果都没有,然后反向递归onTouchEvent()

如果父view拦截事件“onInterceptTouchEvent()返回ture”后面的所有子view收不到任何事件;

如果子view设置了点击事件或者本身view是可点击的(即使没有设置点击事件),点击子view的时候所有父view都收不到onTouchEvent事件

    public boolean onTouchEvent(MotionEvent event) {

        /*
         getAction()是以前的方法,多点触碰后新增getActionMasked()
         两个方法没有任何性能区别,只不过getAction()不支持多点触碰
         */
        switch (event.getActionMasked()) {
            // 第一个手指按下
            case MotionEvent.ACTION_DOWN:
                break;
            // 手指移动
            case MotionEvent.ACTION_MOVE:
                break;
            // 手势被取消
            case MotionEvent.ACTION_CANCEL:
                break;
            // 最后一个手指抬起
            case MotionEvent.ACTION_UP:
                break;
            // 多个手指按下
            case MotionEvent.ACTION_POINTER_DOWN:
                break;
            // 手指离开了触摸屏(有其他手指还在触摸屏上,最后一根手指抬起不会触发)
            case MotionEvent.ACTION_POINTER_UP:
                break;

        }

        /*
         返回true可理解为所有权的标志,返回true代表消费了事件,后续的所有事件都不会往下传递
         这里返回true和在down事件返回true一样,当down事件返回true之后就代表消费事件
         */
        return true;
    }

想要实现长按,双击,缩放等事件仅通过onTouchEvent()方法不太容易,可借助GestureDetectorCompat 和 ScaleGestureDetector 实现;

public class ViewC extends View implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {

    /*
   手势侦测器
    GestureDetector和GestureDetectorCompat是一个东西,一般带Compat都是为了兼容性出的
    使用GestureDetectorCompat需要三步:
       ①实现对应方法
       ②重写onTouchEvent方法,修改返回值为GestureDetectorCompat.onTouchEvent(event);
       ③如果需要双击事件,修改返回值为GestureDetectorCompat.setOnDoubleTapListener(this)
    */
    private GestureDetectorCompat gestureDetector;

    public ViewC(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    {
        // 创建手势侦测器,需要重写onTouchEvent
        gestureDetector = new GestureDetectorCompat(getContext(), this);
        // 设置双击监听
        gestureDetector.setOnDoubleTapListener(this);

        /*
         只实现双击监听可用下面简便写法
         ① onDown 返回true
         ② 重写onTouchEvent()返回 detector.onTouchEvent(event)
         */
//        gestureDetector=new GestureDetectorCompat(getContext(),new GestureDetector.SimpleOnGestureListener(){
//            @Override
//            public boolean onDown(MotionEvent e) {
//                return true;
//            }
//
//            @Override
//            public boolean onDoubleTap(MotionEvent e) {
//                return super.onDoubleTap(e);
//            }
//        });

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 使用GestureDetector
        return gestureDetector.onTouchEvent(event);
    }

    /*
        收到ACTION_DOWN后onDown()就会被调用
            onDown()决定事件是否被消费,true为消费
            如果这里不消费,下面所有的实现方法都无效
        */
    @Override
    public boolean onDown(MotionEvent e) {
        return true;
    }

    // 预按下事件(父view是滑动控件会出现预按下事件)
    @Override
    public void onShowPress(MotionEvent e) {
    }

    /*
    单击:每次按下抬起都会有响应(返回值无用,只有onDown里面的返回值有用)
    如果支持长按事件(按下500ms触发长按事件),在按下500ms后抬起不会触发onSingleTapUp()
    总:如果长按事件打开,按下后500ms内抬起触发;
     如果长按事件未打开,按下后无论何时抬起都会触发

     detector.setIsLongPressEnabled(false); //  关闭长按事件
    */
    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    /**
     * 同步onMove(),和onMove()一样
     * 实现功能:图片放大后跟随手指滑动偏移
     *
     * @param downEvent 按下的事件
     * @param event     当前事件
     * @param distanceX 上一个点到当前点之间距离
     * @param distanceY 上一个点到当前点之间距离
     * @return 没用
     */
    @Override
    public boolean onScroll(MotionEvent downEvent, MotionEvent event, float distanceX, float distanceY) {
        return false;
    }

    // 长按事件
    @Override
    public void onLongPress(MotionEvent e) {
       
    }

    // 惯性滑动,实现fling效果  velocity(速度:抬手一瞬间的 距离/时间 )
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return false;
    }

    /*
    onSingleTapUp 单击事件 (长按事件未开启的话,只要手指按下抬起就会触发)
    onSingleTapConfirmed 单机确认事件
    当设置双击监听打开后,想要使用单机监听用onSingleTapConfirmed方法,
    否则双击屏幕会先调用onSingleTapUp方法,onDoubleTap
    */
    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        return false;
    }

    // 双击事件(两次触摸间隔<300ms)
    @Override
    public boolean onDoubleTap(MotionEvent e) {
        return false;
    }

    // 双击不松手滑动触发
    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
        return false;
    }

}
public class ViewC extends View implements ScaleGestureDetector.OnScaleGestureListener {

    /*
    缩放手势侦测器
    使用方法和GestureDetectorCompat类似,需要两步:
    ①实现对应方法
    ②重写onTouchEvent方法,修改返回值为ScaleGestureDetector.onTouchEvent(event);
     */
    private ScaleGestureDetector scaleGestureDetector;

    public ViewC(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    {
        scaleGestureDetector = new ScaleGestureDetector(getContext(), this);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 使用GestureDetector
        return scaleGestureDetector.onTouchEvent(event);
    }

    // 在scale的时候
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        // 倍数
        float scaleFactor = detector.getScaleFactor();
        // 焦点
        detector.getFocusX();
        detector.getFocusY();
        return false;
    }

    // 在scale之前,可做初始化工作
    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        // 使用放缩必须返回true
        return true;
    }

    // 在scale之后
    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {

    }
    
}

如果想要同时使用双击,长按,缩放手势,可在onTouchEvent()方法中根据触摸屏幕手指数量判断;

    public boolean onTouchEvent(MotionEvent event) {
        // 使用GestureDetector
        if (event.getPointerCount()>1){
            return scaleGestureDetector.onTouchEvent(event);
        }else {
            return gestureDetector.onTouchEvent(event);

        }
    }

根据手势监听器实现一个图片双击缩放,滑动偏移加上fling效果的demo

/**
 * ① 计算图片smallScale和bigScale  ----> 通过 canvas.scale()方法实现放大缩小
 * ② 重写双击事件                  ----> 通过 GestureDetectorCompat 实现双击事件监听
 * ③ 根据双击事件切换scale         ----> 设置标识记录大图小图
 * ④ 添加动画                      ----> 通过 ObjectAnimator 实现大图小图切换动画
 * ⑤ 实现滑动偏移                  ----> 在onScroll() 方法中实现手指滑动偏移
 * ⑥ 实现fling效果                 ----> 在onFling() 方法中通过OverScroller实现fling效果
 */
public class ScalableImageView extends View implements Runnable {

    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private Bitmap bitmap;

    // 记录手指滑动距离
    float offsetX;
    float offsetY;

    float originalOffsetX;
    float originalOffsetY;

    float smallScale;
    float bigScale;
    // 判断是否是大图
    boolean big;

    // 动画
    float scaleFraction;  // 0 - 1f
    ObjectAnimator scaleAnimator;

    // 手势侦测器
    private GestureDetectorCompat detector;
    CustomGestureListener gestureListener = new CustomGestureListener();

    /*
    在onFling中使用,OverScroller算是一个计算器,需要在onFling中设置参数
     */
    private OverScroller overScroller;

    public ScalableImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    {
        bitmap = getAvatar((int) UnitUtil.dp2px(300));
        detector = new GestureDetectorCompat(getContext(), gestureListener);
        overScroller = new OverScroller(getContext());
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        originalOffsetX = (getWidth() - bitmap.getWidth()) / 2f;
        originalOffsetY = (getHeight() - bitmap.getHeight()) / 2f;

        if ((float) bitmap.getWidth() / (float) bitmap.getHeight() > (float) getWidth() / (float) getHeight()) {
            smallScale = (float) getWidth() / (float) bitmap.getWidth();
            bigScale = (float) getHeight() / (float) bitmap.getHeight();
        } else {
            smallScale = (float) getHeight() / (float) bitmap.getHeight();
            bigScale = (float) getWidth() / (float) bitmap.getHeight();
        }

        bigScale = bigScale * 2f;

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 给随手指偏移 乘以scaleFraction在缩小的时候自动居中
        canvas.translate(offsetX * scaleFraction, offsetY * scaleFraction);

        // float scale = big ? bigScale : smallScale;
        // 双击的时候给随这动画的执行scaleFraction会变化
        float scale = smallScale + (bigScale - smallScale) * scaleFraction;
        canvas.scale(scale, scale, getWidth() / 2, getHeight() / 2);

        // 图片居中
        canvas.drawBitmap(
                bitmap,
                originalOffsetX, originalOffsetY,
                mPaint
        );
    }

    // 使用GestureDetectorCompat
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return detector.onTouchEvent(event);
    }

    @SuppressLint("NewApi")
    @Override
    public void run() {
        // 动画是否还在进行中,如果结束了直接return
        if (!overScroller.computeScrollOffset()) return;
        // 更新xy值,计算之后可获取最新的值(每次获取的时候都需要让scroller先计算一下)
        overScroller.computeScrollOffset();
        offsetX = overScroller.getCurrX();
        offsetY = overScroller.getCurrY();
        invalidate();
        postOnAnimation(this);
    }

    public float getScaleFraction() {
        return scaleFraction;
    }

    // 动画使用,set方法里面要加invalidate()刷新
    public void setScaleFraction(float scaleFraction) {
        this.scaleFraction = scaleFraction;
        invalidate();
    }

    private ObjectAnimator getScaleAnimator() {
        if (scaleAnimator == null) {
            scaleAnimator = ObjectAnimator.ofFloat(this, "scaleFraction", 0, 1);
        }
        return scaleAnimator;
    }

    Bitmap getAvatar(int width) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        // inJustDecodeBounds为true,不返回bitmap,只返回这个bitmap的尺寸
        options.inJustDecodeBounds = true;
        // 从资源中读取(比较浪费资源,所以上面设置为true,只获取图片宽高)
        BitmapFactory.decodeResource(getResources(), R.drawable.ysdry, options);
        // 再设置为false,最后要返回bitmap
        options.inJustDecodeBounds = false;
        // 根据缩放比例重新计算宽高
        options.inDensity = options.outWidth;
        options.inTargetDensity = width;
        return BitmapFactory.decodeResource(getResources(), R.drawable.ysdry, options);
    }

    class CustomGestureListener extends GestureDetector.SimpleOnGestureListener {

        @Override
        public boolean onDown(MotionEvent e) {
            return true;
        }

        // 预按下事件(父view是滑动控件会出现预按下事件)
        @Override
        public void onShowPress(MotionEvent e) {
        }

        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return false;
        }

        /**
         * 图片放大后跟随手指滑动偏移
         *
         * @param downEvent 按下的事件
         * @param event     当前事件
         * @param distanceX 上一个点到当前点之间距离
         * @param distanceY 上一个点到当前点之间距离
         * @return 没用
         */
        @Override
        public boolean onScroll(MotionEvent downEvent, MotionEvent event, float distanceX, float distanceY) {

            // 大图的时候移动,设置边界
            if (big) {
                offsetX -= distanceX;
                // (图片宽度 - 屏幕宽度)/ 2
                offsetX = Math.min(offsetX, (bitmap.getWidth() * bigScale - getWidth()) / 2);
                offsetX = Math.max(offsetX, -(bitmap.getWidth() * bigScale - getWidth()) / 2);
                offsetY -= distanceY;
                offsetY = Math.min(offsetY, (bitmap.getHeight() * bigScale - getHeight()) / 2);
                offsetY = Math.max(offsetY, -(bitmap.getHeight() * bigScale - getHeight()) / 2);
            }

            invalidate();
            return false;
        }

        // 长按事件
        @Override
        public void onLongPress(MotionEvent e) {
        }

        // 惯性滑动,实现fling效果  velocity(速度:抬手一瞬间的 距离/时间 )
        @SuppressLint("NewApi")
        @Override
        public boolean onFling(MotionEvent downEvent, MotionEvent event, float velocityX, float velocityY) {

            if (big) {
                // 不会自动刷新,只是设置好了overScroller,可通过overScroller计算和设置动画
                overScroller.fling(
                        // 初始位置(原点)
                        (int) offsetX, (int) offsetY,
                        // velocity(速度:抬手一瞬间的 距离/时间)
                        (int) velocityX, (int) velocityY,
                        // x最大最小范围
                        -(int) (bitmap.getWidth() * bigScale - getWidth()) / 2, (int) (bitmap.getWidth() * bigScale - getWidth()) / 2,
                        // y最大最小范围
                        -(int) (bitmap.getHeight() * bigScale - getHeight()) / 2, (int) (bitmap.getHeight() * bigScale - getHeight()) / 2,
                        // 过渡滚动(类似IOS效果)
                        100, 100
                );

            /*
              我们需要一个循环,不停的修改offsetX,offsetY进行刷新页面
              post()在主线程立即执行,postOnAnimation()在下一帧去主线程执行
              需要做动画,需要一帧一帧执行的时候用postOnAnimation()
              postOnAnimation作用:下一帧的时候执行run()方法
            */
            postOnAnimation(ScalableImageView.this);

            }

            return false;
        }

        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            return false;
        }

        // 双击事件(两次触摸间隔<300ms)
        @Override
        public boolean onDoubleTap(MotionEvent e) {
            big = !big;
            // invalidate();
            if (big) {
                // 根据手指点击的位置进行偏移量计算,根据手指位置方法
                offsetX = (e.getX() - getWidth() / 2f) - (e.getX() - getWidth() / 2) * bigScale / smallScale;
                offsetY = (e.getY() - getHeight() / 2f) - (e.getY() - getHeight() / 2) * bigScale / smallScale;
                getScaleAnimator().start();
            } else {
                getScaleAnimator().reverse();
            }
            // 这里返回无所谓,只要onDown返回true即可
            return false;
        }

        // 双击不松手滑动触发
        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            return false;
        }
    }

}

想实现缩放,添加缩放手势监听

 

Android 自定义View----触摸反馈_第2张图片  Android 自定义View----触摸反馈_第3张图片

你可能感兴趣的:(自定义view)