Android 自定义View学习(十四)——View滑动学习

学习资料:

  • Android群英传
  • Android艺术探索

滑动效果就是实现动态修改一个View的坐标。
实现滑动效果的基本思想:
手指落在屏幕触控屏幕时,系统记下当前的触摸点坐标;手指在屏幕移动时,系统记下移动后的触摸点坐标,获取到每一次相对前一次触摸点坐标的偏移量,通过偏移量来修改View的坐标,不断重复,实现整个滑动过程

1.系统辅助类 <p>

MotionEvent一样,滑动事件系统还提供了另外一些类


1.1 TouchSlop最小距离 <p>

TouchSlop是系统识别最小的滑动距离,是一个常量值。当手指在屏幕滑动距离小于这个值时,系统不会将动作视为滑动。这个常量值的具体大小和设备也有关,不同的屏幕分辨率,可能会不一样

获得方式:

ViewContfiguration.get(getConetxt()).getScaledTouchSlop()

利用这个临界值,可以将一些不想要的手指操作给过滤掉


1.2 VelocityTracker 速度追踪 <p>

用于追踪手指在滑动过程中的速度,包括水平速度和竖直方向的速度

使用过程:

  • 第1步,在View.onToucheEvent()获取VelocityTracker对象
  • 第2步,使用拿到的VelocityTracker对象来计算x,y轴方向的速度
  • 第3步,在比较恰当及时的时机,将VelocityTracker对象释放掉,回收内存

代码:

public class ScrollerActivity extends AppCompatActivity {
    private VelocityTracker velocityTracker;
    private final String TAG = "ScrollerActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scroller);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //获取VelocityTracker
        velocityTracker = VelocityTracker.obtain();
        velocityTracker.addMovement(event);
        //计算滑动速度
        velocityTracker.computeCurrentVelocity(1000);//计算速度
        float xVelocity = velocityTracker.getXVelocity();
        float yVelocity = velocityTracker.getYVelocity();
        Log.e(TAG,"&&&-->x = "+xVelocity+"---> y = "+yVelocity);
        return super.onTouchEvent(event);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (null != velocityTracker){
            velocityTracker.clear();//重置
            velocityTracker.recycle();//回收内存
        }
    }
}

直接在Acvitity测试,获取速度的结果,

  • x轴速度,从左向右滑动时,速度为正,从右向左滑动为负;
  • x轴速度,从上向下滑动时,速度为正,从下向上滑动为负;

正负值就是要看滑动的方向和x,y轴方向是否一致


注意:
在使用velocityTracker.getXVelocity(),velocityTracker.getYVelocity()获取速度之前,要先根据设置的单位时间来计算速度。计算公式v = (终点- 起点) /t。计算出来的速度是相对于设置的时间的。

计算出来的速度指的是一段时间内滑过的像素数。

velocityTracker.computeCurrentVelocity(t)

t = 1000,在1000ms内,假设匀速水平滑过了1000px,水平速度就是1000,也就是1000px/1000ms
t = 100,在100ms内,假设匀速水平滑过了100px,水平速度就是100,也就是100px/100ms


1.3 GestureDetector 手势检测 <p>

用于辅助检测单击、滑动、长按、双击

使用步骤:

  • 第1步:创建GestureDetector对象,并实现OnGestureListener接口。
  • 第2步:接管目标ViewonTouhEvent方法

GestureDetector.setOnDoubleTapListener(onDoubleTapListener)可以实现双击

Activicty为目标View代码:

public class ScrollerActivity extends AppCompatActivity {
    private Toast toast;
    private GestureDetector mGestureDetector;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scroller);
        initGestureDetector();
    }

    /**
     * 初始化 GestureDetector
     */
    private void initGestureDetector() {
        mGestureDetector = new GestureDetector(ScrollerActivity.this,onGestureListener );
        //解决屏幕长按后无法拖动
        mGestureDetector.setIsLongpressEnabled(false);
    }

    private GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {
        @Override
        public boolean onDown(MotionEvent e) {//手指轻触屏幕的一瞬间,由一个ACTION_DOWN触发
            showToast("轻触一下");
            return true;
        }

        @Override
        public void onShowPress(MotionEvent e) {//手指轻触屏幕,尚未松开或拖动,由一个ACTION_DOWN触发
            showToast("轻触未松开");
        }

        @Override
        public boolean onSingleTapUp(MotionEvent e) {//手指离开屏幕,伴随一个ACTION_UP触发,单击行为
            showToast("单击");
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {//手指按下屏幕并拖动
            // 由一个由一个ACTION_DOWN,多个ACTION_MOVE触发,是拖动行为
            showToast("拖动");
            return false;
        }

        @Override
        public void onLongPress(MotionEvent e) {//长按
            showToast("长按");
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            //按下屏幕,快速滑动后松开,由一个由一个ACTION_DOWN,多个ACTION_MOVE,一个ACTION_UP触发
            showToast("快速滑动");
            return false;
        }
    };

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean consume = mGestureDetector.onTouchEvent(event);
        return consume;
    }

    /**
     * Toast
     */
    private void showToast(String str) {
        if (null == toast) {
            toast = Toast.makeText(ScrollerActivity.this, str, Toast.LENGTH_LONG);
        } else {
            toast.setText(str);
        }
        toast.show();
    }
}

OnGestureListeneronDown(),onSingleTapUp(),onScroll(),onFling()方法都有一个boolean类型的返回值,这个值表示是否消费事件


1.4 Scroller 弹性滑动对象 <p>

用于实现View的弹性滑动。Scroller本身无法实现弹性滑动,需要配合ViewcomputeScroll()方法

Scroller使用有个固定的3步走模式:

  1. 初始化Scroller对象
  2. 重写ViewcomputeScroll()方法
  3. 调用mScroller.startScroll()方法

简单使用:

public class ScrollerView extends LinearLayout {
    private Scroller mScroller;

    public ScrollerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initScroller();
    }

    /**
     * 初始化Scroller
     */
    private void initScroller() {
        mScroller = new Scroller(getContext());
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {//判断Scroller是否执行完毕
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

    public void smoothScrollTo(int destX, int destY) {
        //计算相对于左上角的偏移量
        final int deltaX = getScrollX() - destX;
        final int deltaY = getScrollY() - destY;
        //在1000ms内滑向destX destY
        mScroller.startScroll(0, 0, deltaX, deltaY, 1000);
        invalidate();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                smoothScrollTo((int) event.getX(), (int) event.getY());
                break;
            case MotionEvent.ACTION_UP://恢复左上角
                mScroller.startScroll(getScrollX(), getScrollY(), -getScrollX(), -getScrollY(), 1000);
                invalidate();
                break;
        }
        return true;
    }
}

效果便是手指点在屏幕哪里,一秒内,ScrollerView内的所有子控件便会滑动到手指落的点的位置

关于Scroller这里先了解一点点,打算之后再单独来学习


2.实现滑动的7种方法 <p>

Android群英传中,徐医生给出7种滑动方法:

  1. layout方法
  2. offsetLetAndRight()和offsetTopAndBottom()
  3. LayoutParams
  4. scrollTo和scrollBy
  5. Scroller
  6. 属性动画
  7. ViewDragHelper

5上面刚刚有了解,以后还会继续补充学习,6Android动画基础知识学习(下)学习了解过。1234在本篇会学习了解,这几个方法感觉效果都不是很好,滑动效果很突兀,最重要的便是方法7,下篇单独来学习


2.1ayout方法

View进行绘制时,会调用onLayout()方法来设置显示的位置

代码:

public class ScrollerView extends LinearLayout {

    private float lastX, lastY;

    public ScrollerView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                //计算偏移量
                float offsetX = x - lastX;
                float offsetY = y - lastY;
                //计算四个顶点的位置
                int left = (int) (getLeft() + offsetX);
                int top = (int) (getTop() + offsetY);
                int right = (int) (getRight() + offsetX);
                int bottom = (int) (getBottom() + offsetY);
                //布局回调
                layout(left, right, top, bottom);
                break;
        }
        return true;
    }
}

不晓得是我代码有问题还是这个思路本身有问题,体验非常不好,childView在滑动过程中,大小会发生改变


2.2 offsetLeftAndRight和offsetTopAndBottom <p>

系统提供的对左右上下移动的API的封装,效果和使用与layout方法类似

layout方法代码简单修改:

case MotionEvent.ACTION_MOVE:
    //计算偏移量
    float offsetX = x - lastX;
    float offsetY = y - lastY;
    Log.e("offset","&&&--"+offsetX+"-->"+offsetY);
    offsetLeftAndRight((int)offsetX);
    offsetTopAndBottom((int)offsetY);
break;

子控件会随着手指在屏幕滑动而滑动

offset有效区域

这个方法遇到个问题,有些区域无效,只有黄色边框内才有效


2.3 LayoutParams 布局参数 <p>

LayoutParams保存了一个View的布局参数。可以通过LayoutParams来动态地修改一个布局的位置参数。

简单的修改代码:

 case MotionEvent.ACTION_MOVE:
    //计算偏移量
    float offsetX = x - lastX;
    float offsetY = y - lastY;
    Log.e("offset", "&&&--" + offsetX + "-->" + offsetY);
    MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
    layoutParams.leftMargin = (int) (getLeft() + offsetX);
    layoutParams.topMargin = (int) (getTop() + offsetY);
    setLayoutParams(layoutParams);
break;

同样有一个有效区域的问题


2.4 使用scrollTo或者scrollBy <p>

两个方法区别: scrollBy()相对移动,scrollTo()绝对移动

View内部有两个属性mScrllXmScrollY,分别可以通过getScrollX()getScrollY()方法得到

在滑动过程中,mScrollX总是等于View左边缘和View内容左边缘在水平方方向的距离;mScrollY总是等于View上边缘和View中内容上边缘在竖直方向的距离。View边缘是指View的位置也就是View的四个顶点到父容器的距离,View内边缘是内容距离View四边的距离。

无论是scrollTo()还是scrollBy()都只能改变View内容的位置而不能改变View在布局中的位置

mScrollX/Y单位为像素px。当View左边缘在View内容左边缘右边时,mScrollX为正值,反之为负值;同理,当View上边缘在View内容上边缘下边时,mScrollX为正值,反之为负值。也就是说,View从左向右滑动,mScrollX为负值,反之为正值;从上往下滑动,mScrollY为负值,反之为正值

mScrollX和mScrollY变换规律

白色为View原始位置,紫色矩形为内容


简单使用:

 case MotionEvent.ACTION_MOVE:
    //计算偏移量
    float offsetX = x - lastX;
    float offsetY = y - lastY;
    Log.e("offset", "&&&--" + offsetX + "-->" + offsetY);
    //scrollBy((int)-offsetX,(int)-offsetY);
    scrollTo((int)-offsetX,(int)-offsetY);
    break;

根据规律图,手势和实际移动方向相反,在设置参数时,设置为了-offsetX


几种方式,简单总结

  • scrollTo/By:操作简单,适合对View内容的滑动
  • 属性动画:操作简单,适用于没交互View和实现复杂的动画效果
  • 改变布局参数:操作复杂,适用于有交互的View

3. 滑动冲突

滑动冲突常见的场景:

  1. 外部滑动方向和内部滑动方向不一致
  2. 外部滑动方向和内部滑动方向一致
  3. 上面两种情况嵌套
滑动冲突

解决方式有两种:外部拦截,内部拦截


3.1 外部拦截 <p>

外部拦截思路:
点击事件都会先经过父容器的拦截处理,如果父容器需要处理此事就拦截,否则就不进行拦截。重写父容器的onInterceptTouchEvent()方法

伪码:

public boolean onInterceptTouchEvent(MotionEvent event){
       boolean intercepted = false;
       int x = (int) event.getX();
       int y = (int) event.getY();
       switch(event.getAction()){
             case MotionEvent.ACTION_MOVE:
                  intercepted = true;
             break;
             case MotionEvent.Move:
                  if(父容器需要当前点击事件){
                     intercepted = true;
                  }else{
                     intercepted = false; 
                  }
             break;
             case MotionEvent.ACTION_UP:
                  intercepted = false;
             break;
      }
      mLastXIntercept = x;
      mLastYIntercept = y;
      return intercepted;
}

处理思路代码基本都是固定的。
首先,在ACTION_DOWN中,父容器必须返回false,不拦截ACTION_DOWN事件。因为一旦拦截了ACTION_DOWN后续的ACTION_MOVEACTION_UP都会又父容器来处理,这样事件就无法传递给childView
其次,在ACTION_MOVE中,可以根据需要来进行拦截,需要就返回true,否则就false
最后,在ACTION_UP中,返回false


注意:
如果父容器在ACTION_UP中,返回了truechildView就不会再收到ACTION_UP事件,childViewonClick事件就不会触发。父容器比较特殊,一旦开始拦截某个事件,之后的序列事件都是交给父容器来处理,包括ACTION_UP,即使在ACTION_UP中返回falseACTION_UP还是由父容器处理


3.2 内部拦截 <p>

内部拦截法指的是父容器不拦截任何事件,所有的事件都传递给childView,根据需要,childView来选择是否消费,需要配合requestDisallowInterceptTouchEvent()方法。重写childViewdispatchTouchEvent()方法

伪码:

public boolean dispatchTouchEvent(MotionEvent event){
       int x = (int) event.getX();
       int y = (int) event.getY();
       switch(event.getAction()){
          case MotionEvent.ACTION_DOWN:
               parent.requestDisallowInterceptTouchEvent(true);
          break;
          case MotionEvent.ACTION_MOVE:
               int deltaX = x - mLastX;
               int deltaY = y - mLastY;
               if(父容器需要此类点击事件){
                   parent.requestDisallowInterceptTopuchEvent(false);
               }
          break;
          case MotionEvent.ACTION_UP:
               break;
          break;
       }
       mLastX = x ;
       mLastY = y ;
       return super.dispatchTouchEvent(event);
}

使用稍微比外部麻烦。

ACTION_DOWN中,使用parent.requestDisallowInterceptTouchEvent(true),让父容器不拦截ACTION_DOWN事件,ACTION_DOWN不受FLAG_DISALLOW_INTERCEPT标记位控制


4.最后 <p>

国庆放假在家的效率有些低,事有点多。农村娃,还下地干了会活,哈哈。打算将自定义系列结束呢,完不成不计划了。按照学习计划,还剩下2篇学习内容

本人很菜,有错误请指出

共勉 :)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容