Android View的滑动

私以为View的滑动可能说成View的移动更好理解,无论是跟从手指触点对View进行移动还是响应用户操作逻辑所作出的对View进行的滑动效果,还是将View进行移动来体现的,比如说RecyclerView的滑动是将单独Item进行移动。

一、实现移动

首先需要意识到一点,我们对View进行移动的时候,应该通过View本身来捕获触点的坐标,也就是需要重写View内部的onTouchEvent方法或者外部设置onTouch监听来实现,而不是外部ViewGroup来确定。

1.1 layout()

由于View在进行绘制的时候会调用到onLayout方法来确认自身的显示位置,而一个View位置的确定由:left,top,right,bottom这四个参数来确定,所以我们可以在onTouchEvent中获取触点的坐标,然后根据坐标计算出对应的参数值设置给layout方法,而每次调用layout方法,都会对View进行重新绘制,这样就可以达到移动的效果。

关键代码如下:

//记录down事件时候的触点
float downX,downY;
@Override
public boolean onTouchEvent(MotionEvent event) {
	//捕获当前事件坐标
	int x = (int) event.getX();
	int y = (int) event.getY();
	switch (event.getAction()){
		case MotionEvent.ACTION_DOWN:
			downX = event.getX();
			downY = event.getY();
			break;
		case MotionEvent.ACTION_MOVE:
			//计算偏移量
			int offsetX = (int) (x - downX);
			int offsetY = (int) (y - downY);
			//赋值偏移量
			layout(getLeft()+offsetX,getTop()+offsetY,getRight()+offsetX,getBottom()+offsetY);
                    break;
        }
	return super.onTouchEvent(event);
    }

1.2 设置位置偏移量

在通过layout方法进行View的移动的时候,关键在于计算出手指当前触点(MOVE事件得到的坐标)相对与触发DOWN事件的时候所记录下的坐标的偏移量。而View自身也提供了offsetLeftAndRight()与offsetTopAndBottom()这两个方法来实现便宜,将MOVE中计算得到的便宜量传入即可。

1.3 改变布局参数

通过改变布局参数同样可以实现View的移动,LayoutParams中保存了各个View的位置信息,不同的ViewGroup有着不同的LayoutParams,但都是继承自ViewGroup.MarginLayoutParams,在子View中可以通过getLayoutParams方法获取父ViewGroup的布局参数对象,即不同的Layout有着不同的LayoutParams,需要根据实际情况进行来,但也可以使用MarginLayoutParams来实现View的滑动,这个是无论Layout的类别的。

LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) getLayoutParams();
//对marigin属性累加,累加的值就是每一次的偏移量,从而实现移动效果
lp.leftMargin += offsetX;
lp.topMargin +=  offsetY;
setLayoutParams(lp);
//requestLayout();也可以

1.4 动画

平移动画可以实现将View移动的效果,需要留意的是如果使用View动画进行平移,它存在的不足(并没有实际改变View的位置),如果使用属性动画进行平移,使用对应于平移的动画属性translationX。

简单代码如下:

//将View从原来的位置移动到300,300
ObjectAnimator.ofFloat(tvMove,"translationX",0,300).setDuration(1000).start();
ObjectAnimator.ofFloat(tvMove,"translationY",0,300).setDuration(1000).start();

使用动画的适用场景是对View的移动过程不需要响应用户交互,如果说要通过对用户触点的追踪加上动画来实现对View的实时移动,效果可能会是很糟糕。但对于不需要相应用户交互的View的移动,动画却是一种非常简便的实现方式,尤其是对于那些移动效果很复杂的交互设计而言更是如此。

1.5 ScrollTo以及ScrollBy

scrollTo(x,y),scrollBy(dx,dy)

两个方法的作用都是将View进行移动,to的参数是移动的终点坐标(与起点无关),by的参数是起点想对于终点的偏移量(起点的坐标会影响最终的移动位置)。

//如果是View,移动的就是它的内容,比如TextView的Text
this.scrollBy(-offsetX,-offsetY);
//如果是ViewGroup,移动的就是它的>>所有<<子View
((View)getParent).scrollBy(-offsetX,-offsetY);

为了达到我们的目的效果,我们将传入scrollBy的偏移量置反,原因很简单,移动方向的参考系不同,而实际上可以认为系统移动的不是View(或者画布)而是屏幕。

想象一个场景:

透过放大镜看报纸:

我们错误习惯性思维是:移动方向指的就是内容区域,也就是画布(报纸)的移动方向,我们以为它是跟随我们的手指移动方向移动的,当我们向下(向右)移动的时候,画布(报纸)是向左下角移动。

但是系统认为,画布(报纸)的位置是固定不变的,我们能看到的只有屏幕(放大镜)里面的区域,但并不代表屏幕(放大镜)外面的区域不存在,它只是超出了显示范围而已,scrollBy传入正值,实际上是将屏幕方向向右下方移动,于是画布相对于屏幕而言就是向左上角(也就是我们看到的朝相反的方向移动)移动。



所以为了体现跟随手指移动的效果,需要将偏移量置反。

scrollBy的内部调用到了scrollTo,是对to方法的一个扩展封装。

public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

关于滑动实现的原理,从源码来看,还是借助不断重绘来实现的,这里就不贴代码了。

1.6 使用Scroller

scrollTo和scrollBy两个方法固然可以实现View的移动,不足在于,移动效果是瞬时完成的,我们之所以能用这两个API来实现跟随手指移动的效果,那也是不断调用他们每次瞬时移动一小段距离来实现的,实质上这也是实现弹性滑动(有一个渐进的过程)的思想:把”一瞬间”大的滑动分割成若干次”一瞬间“小的滑动,除了使用动画(设置duration的值)之外,还有就是这个Scroller了,当然,利用线程的延时策略也可以。

Scroller本身无法实现弹性滑动,需要配合View的computeScroll方法来实现。

典型使用方式:

a.首先创建Scroller对象:

mScroller = new Scroller(context);

b.重写View的computeScroll方法:

@Override
public void computeScroll() {
    super.computeScroll();
    if (mScroller.computeScrollOffset()){
        scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
        invalidate();
    }
}

c.调用Scroller的startScroll方法:

public void smoothScrollTo(int destX,int destY){
    //startX,startY,dX,dY,duration
	mScroller.startScroll(getScrollX(),getScrollY(),destX,destY,1000);
	invalidate();
}

1.7 几种滑动总结:

1.scrollTo和scrollBy适用于对View内容的移动(ViewGroup的内容就是他的子View啦)。

2.动画操作简单,适用于不需要响应用户交互的移动。

3.如果要响应用户交互的移动,改变布局参数的方式更为理想。

二、Scroller解析

上面回顾了Scroller的使用方式,首先创建对象然后重写View的comoute方法,最后调用startScroll方法就可以了,下面就探究一下Scroller是怎样一个工作流程。上面的smoothScrollTo方法内部就调用了startScroll和invalidate两个方法,所以主要也就围绕这两个方法的源码展开:

a.startScroll

传入的参数上面已经标注过了,直接贴代码:

public void startScroll(int startX, int startY, int dx, int dy, int duration) {
    mMode = SCROLL_MODE;
    mFinished = false;
    mDuration = duration;
    mStartTime = AnimationUtils.currentAnimationTimeMillis();
    mStartX = startX;
    mStartY = startY;
    mFinalX = startX + dx;
    mFinalY = startY + dy;
    mDeltaX = dx;
    mDeltaY = dy;
    mDurationReciprocal = 1.0f / (float) mDuration;
}

赋值赋值赋值,整个方法做的工作就这一个。将滑动的起始坐标,终点坐标统,滑动间隔时间等传入View中。

b.invalidate

该方法是Android提供的View更新的方法之一(另一个是postInvalidate),二者在使用上的区别在于前者在主线程(UI线程中使用),后者是在子线程(非UI线程中使用),通过调用该方法,可以让View进行重绘。View绘制的时候会调用到draw方法,在draw方法内会调用到我们重写的computeScroll方法。

流程图如下:



(到这里本想继续探究下去,但涉及的源码量大且与View的绘制有很大关系,故先就此打住,就暂时埋个伏笔以后再写吧)

c.computeScroll

@Override
public void computeScroll() {
	super.computeScroll();
	if (mScroller.computeScrollOffset()){
		scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
		invalidate();
	}
}

super.computeScroll();父View的compute方法同子View基本如出一辙,重点在于,computeScrollOffset这个方法,该方法源码如下

/**
     * Call this when you want to know the new location.  If it returns true,
     * the animation is not yet finished.
     */
    public boolean computeScrollOffset() {
        ...
        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

        if (timePassed < mDuration) {
            switch (mMode) {
            case SCROLL_MODE:
                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                mCurrX = mStartX + Math.round(x * mDeltaX);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
            case FLING_MODE:
                ...
                break;
            }
        }
        else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }

删去了一些数学计算的代码,从源码注释来看,当需要知道View的新的位置的时候调用该方法,返回的值是这个移动的动画是否完毕,看到了这个方法里面有一个熟悉的东西,插值器,他就是根据时间的流逝百分比来计算出新的scrollX和scrollY的坐标,调用完这个方法,我们在compute方法里面直接获取新的scrollXY,直接使用ScrollTo方法即可,就完成了将大段的滑动改为一小段一小段的滑动,从而体现出弹性滑动的效果。

三、View的滑动冲突

View的滑动冲突主要是因为可滑动View出现嵌套导致控件对滑动逻辑出现处理冲突的情况,比如说一个横向的ScrollerView和一个纵向的ScrollerView嵌套在一起,因为两个控件View都能处理滑动事件,可能出现用户希望纵向华东却成了横向滑动,这就是滑动冲突。

3.1 常见的滑动冲突与处理规则

3.1.1 内外滑动方向不一致

做过比较多的ViewPager配合RecyclerView实现轮播的头布局,像这样:



这里存在方向不一致的滑动冲突,但是因为ViewPager已经帮我们处理好了这种冲突,所以我们无需手动去解决它。但如果是遇到像ScrollView这样并没有自动处理这些冲突的可能就得手动处理了。

比如下面这种:

这个是我自己遇到过的例子,当初看鸿洋大佬以前写的实现仿QQ5.0的侧滑菜单(借助了HorizontalScrollView来实现)的时候,后面自己放学后也实践了一下,在测试写的整体效果的时候,为了节省时间我采用了一个纵向的ScrollView来实现内容区域,之后就遇到了滑动冲突,横向侧滑经常打不开菜单区域,后面在3.4部分再详细说明。

解决的办法其实并不难,借助外部拦截法即可。

外部拦截法:

外部拦截法就是将点击事件先经过父容器的拦截方法去处理,如过父容器需要对应的事件就拦截下来,否则就不拦截从而将事件交给子View(子容器)去处理。所以需要重写父容器的onInterceptTouchEvent方法。

3.1.2 内外滑动方向一致

当两个View的滑动方向一致的时候,系统没办法判断用户要哪一层的View来进行滑动,所以就需要开发者根据具体的业务逻辑来处理了,处理的办法可以借助内部拦截法。

这里我借助了两个同向的ScrollView嵌套来实现这样一个业务场景。

内部拦截法

上一篇博客的补充3中提到过一个特殊的标记FLAG_DISALLOW_INTERCEPT,就是靠它来实现事件的内部拦截,子View(子容器)可以获取到父View的实例,通过父View的requestDisallowInterceptTouchEvent方法可以设置这个标志位,子View可以决定父View是否需要拦截对应的事件,子View需要重写dispathcTouchEvent方法,在其内部根据自己的操作逻辑决定是否将这次事件拦截下来。

具体的情景看最后的滑动冲突解决实例。

3.1.3 上述两种情况复合嵌套

复合嵌套引起的滑动冲突的解决方式就是复杂问题简单化了,将每一层的嵌套逐一分解,分别处理各层存在的滑动冲突就行了。

3.2 滑动冲突解决实例

3.2.1 实例1:

3.1.1中已经简单说过了借助ScrollView实现侧滑菜单时候遇到的滑动冲突情景,具体的示意图如下:



因为ScrollView本身不能处理两个方向上的滑动,灰色背景部分是一个横向的ScrollView,只能处理横向的滑动,绿色部分是一个纵向的ScrollView,当在绿色部分左右滑动基本上是不能拉出菜单的,这里就出现了滑动冲突,简单贴一下这个DrawerLayout的代码:

class ScrollDrawerLayout : HorizontalScrollView {
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        ...
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        super.onLayout(changed, l, t, r, b)
        ...
    }

    override fun onTouchEvent(ev: MotionEvent?): Boolean {
        var action = ev?.action
        var x = ev!!.x
        var y = ev!!.y
        var downX:Float = 0F
        when(action){
            MotionEvent.ACTION_DOWN->{
                downX = ev!!.x
            }
            MotionEvent.ACTION_UP->{
              	//手指松开,根据拉出程度决定是打开菜单还是关闭
                isMenuOpen = if (scrollX>=mMenuWidth/2){
                    this.smoothScrollTo(mMenuWidth,0)
                    false
                } else{
                    this.smoothScrollTo(0,0)
                    true
                }
                return true
            }
        }
        return super.onTouchEvent(ev)
    }
    //关键方法
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        var x = ev!!.x
        var y = ev!!.y
        var action = ev?.action
        when(action){
            MotionEvent.ACTION_DOWN->{
                downX = x
                downY = y
            }
            MotionEvent.ACTION_MOVE->{
                var dx = Math.abs(x-downX)
                var dy = Math.abs(y-downY)
                //冲突处理的关键地方
                if (dx==0F||Math.abs(dy/dx)>(1/3F)) return false
                else if (dy==0F||Math.abs(dy/dx)<(1/3F)) return true
            }
        }
        return super.onInterceptTouchEvent(ev)
    }
}

上面的代码删去了一些,处理滑动冲突的关键在于,具体的操作情景是怎样的,比如说这里,我的想法是将根据用户滑动屏幕的轨迹的斜率来处理,如果斜率大于1/3就表示用户是竖向滑动,否则就是横向滑动,要拉出菜单,那么就在外层横向ScrollView的onInterceptTouchEvent中的MOVE事件中编写处理代码,竖向滑动就返回false表示不拦截,事件将交给子LinearLayout(因为ScrollView不允许多个子View(ViewGroup),所以我还嵌套了一个LinearLayout,但没多大关系)下的纵向ScrollView处理,来进行竖向滑动;否则的话返回true表示拦截,那么事件就交到了横向ScrollView这里,就可以响应横向滑动拉出菜单了。

3.3.2 实例2:

示意图如下:



外层嵌套内层,图中绿色的item可以正常滑动,但是当触点在蓝色部分(子ScrollVew)的时候并不能响应内层深蓝色item的滑动,利用内部拦截法处理,不多说直接贴解决的代码:

class MyVerticalScroll:ScrollView {

    var isScrolledToBottom = false
    var isScrolledToTop = false

    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        val ac = ev.action
        when(ac){
        	//当触点落在子ScrollView范围内的时候直接让父ScrollView不要拦截此次事件
            MotionEvent.ACTION_DOWN->{
                parent.requestDisallowInterceptTouchEvent(true)
            }
            MotionEvent.ACTION_MOVE->{
            	//不是滑动到了顶部或者底部的话,就应该将事件交给父ScrollView去处理
                if (isScrolledToTop||isScrolledToBottom){
                    parent.requestDisallowInterceptTouchEvent(false)
                }
            }
        }
        //归位标记位
        isScrolledToBottom = false
        isScrolledToTop = false
        return super.dispatchTouchEvent(ev)
    }
	//对ScrollView的滑动状态进行监听的API,level 9以后才可以,直接获取是否滑动到了顶部或者底部,在这里面给标志滑动位置的标记赋值
    override fun onOverScrolled(scrollX: Int, scrollY: Int, clampedX: Boolean, clampedY: Boolean) {
        super.onOverScrolled(scrollX, scrollY, clampedX, clampedY)
        if (scrollY == 0) {
            isScrolledToTop = clampedY;
            isScrolledToBottom = false;
        } else {
            isScrolledToTop = false;
            isScrolledToBottom = clampedY;
        }
    }
}

上述的核心代码就在dispatchTouchEvent中的parent.requestDisallowInterceptTouchEvent(boolean)这一句。


参考资料:

《Android开发艺术探索》

《Android进阶之光》

《Android群英传》

Android View的滑动的更多相关文章

  1. Android View的滑动 动画

    [scrollTo/scrollBy] //控件内的文字会移动,但是控件本身不会移动,而且移动到控件之外之后,文字也就看不见了 if(v.equals(button2)){ button2.scrol ...

  2. android中实现view可以滑动的六种方法续篇(二)

    承接上一篇,上一篇中讲解了实现滑动的第五种方法,如果你还没读过,可点击下面链接: http://www.cnblogs.com/fuly550871915/p/4985482.html 这篇文章现在来 ...

  3. android中实现view可以滑动的六种方法续篇(一)

    承接上一篇,如果你没有读过前四章方法,可以点击下面的链接: http://www.cnblogs.com/fuly550871915/p/4985053.html 下面开始讲第五中方法. 五.利用Sc ...

  4. android中实现view可以滑动的六种方法

    在android开发中,经常会遇到一个view需要它能够支持滑动的需求.今天就来总结实现其滑动的六种方法.其实每一种方法的 思路都是一样的,即:监听手势触摸的坐标来实现view坐标的变化,从而实现vi ...

  5. Android开源中国客户端学习 (自定义View)左右滑动控件ScrollLayout

    左右滑动的控件我们使用的也是非常多了,但是基本上都是使用的viewpager 等 android基础的控件,那么我们有么有考虑过查看他的源码进行定制呢?当然,如果你自我感觉非常好的话可以自己定制一个, ...

  6. Android View体系(二)实现View滑动的六种方法

    1.View的滑动简介 View的滑动是Android实现自定义控件的基础,同时在开发中我们也难免会遇到View的滑动的处理.其实不管是那种滑动的方式基本思想都是类似的:当触摸事件传到View时,系统 ...

  7. 浅谈Android View滑动和弹性滑动

    引言 View的滑动这一块在实际开发中是非常重要的,无论是优秀的用户体验还是自定义控件都是需要对这一块了解的,我们今天来谈一下View的滑动. View的滑动 View滑动功能主要可以使用3种方式来实 ...

  8. 【朝花夕拾】Android自定义View篇之(十一)View的滑动,弹性滑动与自定义PagerView

    前言 由于手机屏幕尺寸有限,但是又经常需要在屏幕中显示大量的内容,这就使得必须有部分内容显示,部分内容隐藏.这就需要用一个Android中很重要的概念——滑动.滑动,顾名思义就是view从一个地方移动 ...

  9. Android View 的事件体系

    android 系统虽然提供了很多基本的控件,如Button.TextView等,但是很多时候系统提供的view不能满足我们的需求,此时就需要我们根据自己的需求进行自定义控件.这些控件都是继承自Vie ...

随机推荐

  1. selenium + python + nwjs

    1.下载chromedriver文件 http://chromedriver.storage.googleapis.com/index.html google官方下载地址 http://dl.nwjs ...

  2. [c/c++] programming之路(29)、阶段答疑

    一.指针不等于地址 指针不仅有地址,还有类型,是一个存储了地址的变量,可以改变指向:而地址是一个常量 #include<stdio.h> #include<stdlib.h> ...

  3. windows下共享文件夹在Linux下打开

    ①首先在Windows下创建一个准备用来共享用的文件夹 ②在虚拟机下选择第一步创建的文件夹为共享文件夹 ③在虚拟机shell命令框下输入 cd  /mnt/sgfs 回车进入共享文件夹. 备注:其他细 ...

  4. 第二课丶pygame

    学号:2017*****1024 姓名:王劲松 我的码云贪吃蛇项目仓库:https://gitee.com/Danieljs/sesnake 分析游戏中的备注和问题:10分钟 游戏名称.分数改动:3分 ...

  5. criteo marketing api 相关

    官网登陆地址:https://marketing.criteo.com/ 官网api介绍:https://marketing.criteo.com/e/s/article?article=360001 ...

  6. Lintcode155-Minimum Depth of Binary Tree-Easy

    155. Minimum Depth of Binary Tree Given a binary tree, find its minimum depth. The minimum depth is ...

  7. robotframework-ride支持python3

    最近发现robotframework的RIDE工具终于支持python3了,赶紧就安装了一下. 最新版本1.7.3.1基于wxPython4.0.4,此时的wxPython也是支持Python3.x的 ...

  8. clojure开发环境配置git, vscode+Calva插件配置

    万事开头难,全是犄角旮旯的细节. 1 安装lein 参见 https://www.cnblogs.com/xuanmanstein/p/10504401.html 2 创建工程 lein 参考http ...

  9. 【架构设计】Android:配置式金字塔架构

    最近做一个项目,在项目搭建之前,花了些许时间去思考一下如何搭建一个合适的架构.一开始的构思是希望能合理的把应用的各部分进行分离,使其像金字塔一样从上往下,下层为上层提供功能. 在平常项目中,总是有很多 ...

  10. Subversion ----> svnserve.conf / authz / passwd / hooks-env.tmpl <<翻译笔记>>

    svnserve.conf 假如你使用这个文件去允许访问这个仓库,那么这个文件控制着svnserve后台进程的配置.(但是如果你只是允许通过http和/或者 file:URLs,则这个文件就不起作用了 ...