Android View滑动总结

前言

View的滑动是Android自定义控件的基础,在开发中我们难免会遇到View的滑动处理。

其实不管是哪种滑动方式,基本思想都是差不多的:
1,当点击事件传到View时,系统记下触摸点的坐标;
2,手指移动时系统记下移动后触摸的坐标并算出偏移量,并通过偏移量来修改View的坐标;

实现View滑动有很多种方法,这里主要讲下以下6种:

  • 1,layout();
  • 2,offsetLeftAndRight()与offsetTopAndBottom();
  • 3,LayoutParams、
  • 4,动画、
  • 5,scollTo 与 scollBy,
  • 6,Scroller。

1.layout()方式

首先看一下layout(),在View进行绘制时,会调用OnLayout()方法来设置View显示的位置,同时可以修改View的left,top,right,bottom四个属性来控制View的坐标。这个坐标是View坐标系


a.png

代码如下

class DragView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    private var lastX: Int = 0
    private var lastY: Int = 0

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val x = event.x.toInt()
        val y = event.y.toInt()
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                lastX = x
                lastY = y
            }
            MotionEvent.ACTION_MOVE -> {
                val offsetX = x - lastX
                val offsetY = y - lastX
                //四个参数,left,top,right,bottom
                layout(left + offsetX, top + offsetY, right + offsetX, bottom + offsetY)
            }
        }
        return true

    }
}

效果如下

20180730174826839.gif

2,offsetLeftAndRight()与offsetTopAndBottom()方式

这种方式和layout()的方式基本是一样的,根据名字也知道这两个方法分别是设置左边和右边,上面和下面的偏离值

class DragView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private var lastX: Int = 0
    private var lastY: Int = 0

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val x = event.x.toInt()
        val y = event.y.toInt()
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                lastX = x
                lastY = y
            }
            MotionEvent.ACTION_MOVE -> {
                val offsetX = x - lastX
                val offsetY = y - lastX
                offsetLeftAndRight(offsetX)
                offsetTopAndBottom(offsetY)
            }
        }
        return true
    }



}

这样就可以了

3,LayoutParams的方式

LayoutParams主要保存了一个View的布局参数,因此我们可以通过LayoutParams来改变View的布局参
数从而达到改变View位置的效果;

class DragView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    private var lastX: Int = 0
    private var lastY: Int = 0

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val x = event.x.toInt()
        val y = event.y.toInt()
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                lastX = x
                lastY = y
            }
            MotionEvent.ACTION_MOVE -> {
                val offsetX = x - lastX
                val offsetY = y - lastX
                val layoutParams = this.layoutParams as ViewGroup.MarginLayoutParams
                layoutParams.leftMargin = left + offsetX
                layoutParams.topMargin = top + offsetY
                setLayoutParams(layoutParams)
            }
        }
        return true

    }
}

这里如果viewgroup是relayoutlayout,如果设置了android:layout_centerInParent="true"是不起作用的;

4.动画方式

采用动画的方式来进行View的滑动,主要就涉及到两个动画:属性动画和补间动画
补间动画的实现

  • 通过xml的方式
    在res目录新建anim文件夹并创建translate.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"   android:duration="2000">
    <translate
        android:fromXDelta="0"
        android:toXDelta="300"/>
</set>

然后在代码中调用

  dragView.animation = AnimationUtils.loadAnimation(this, R.anim.translate)

效果如下

c.gif

运行程序,我们设置的方块会向右平移300像素,然后又会回到原来的位置。为了解决这个问题,我们
需要在translate.xml中加上fillAfter="true",代码如下所示。运行代码后会发现,方块向右平移300像素后就
停留在当前位置了。

<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" 
     android:duration="2000"
     android:fillAfter="true">
    <translate
        android:fromXDelta="0"
        android:toXDelta="300"/>
</set>

需要注意的是,View动画并不能改变View的位置参数。如果对一个Button进行如上的平移动画操作,
当Button平移300像素停留在当前位置时,我们点击这个Button并不会触发点击事件,但在我们点击这个
Button的原始位置时却触发了点击事件。这就是补间动画和属性动画的区别

当然我们也可以直接通过代码来实现,不用xml,主要使用的类为TranslateAnimation:

   val animation = TranslateAnimation(0f, 300f, 0f, 0f)
        animation.duration = 2000//设置动画持续时间 
        animation.fillAfter=true
        dragView.animation =animation

效果和上面是一样的

属性动画

属性动画是Android3.0之后推出的,就是为了解决补间动画的焦点问题,我们经常使用到的类主要有AnimatorSet和ObjectAnimator;使用 ObjectAnimator 进行更精细化的控制,控制一个对象和一个属性值,而使用多个ObjectAnimator组合到AnimatorSet形成一个动画。属性动画通过调用属性get、set方法来真实地控制一个View的属性值,因此,强大的属性动画框架基本可以实现所有的动画效果。

ObjectAnimator

ObjectAnimator 是属性动画最重要的类,创建一个 ObjectAnimator 只需通过其静态工厂类直接返还一个
ObjectAnimator对象。参数包括一个对象和对象的属性名字,但这个属性必须有get和set方法,其内部会通
过Java反射机制来调用set方法修改对象的属性值。下面看看平移动画是如何实现的,代码如下所示:

       val animation = ObjectAnimator.ofFloat(dragView, "translationX", 300f)
        animation.duration = 2000
        animation.start()

这和补间动画的加了fillAfter属性后动画效果是一样的,但是不一样的地方就是焦点已经平移到了新的位置;

注:
需要注意的是,在使用ObjectAnimator的时候,要操作的属性必须要有get和set方法,不然
ObjectAnimator 就无法生效。如果一个属性没有get、set方法,也可以通过自定义一个属性类或包装类来间
接地给这个属性增加get和set方法。现在来看看如何通过包装类的方法给一个属性增加get和set方法,代码如
下所示:

class MyView(var mTarget: View) {

    fun getWidth(): Int {
        return mTarget.layoutParams.width
    }

    fun setWidth(width: Int) {
        mTarget.layoutParams.width = width
        mTarget.requestLayout()
    }
}

这里我们设置2秒增加view的宽度300个像素,使用时只需要操作包类就可以调用get、set方法了:

     val myView = MyView(dragView)
     ObjectAnimator.ofInt(myView, "width", 300).setDuration(2000).start()

效果如图

d.gif
ValueAnimator

ValueAnimator不提供任何动画效果,它更像一个数值发生器,用来产生有一定规律的数字,从而让调
用者控制动画的实现过程。通常情况下,在ValueAnimator的AnimatorUpdateListener中监听数值的变化,从
而完成动画的变换,代码如下所示:

        val valueAnimator = ValueAnimator.ofFloat(0f, 100f)
        valueAnimator.setTarget(dragView)
        valueAnimator.duration = 2000
        valueAnimator.start()
        valueAnimator.addUpdateListener({
            val animatedValue = it.animatedValue as Float
        })

这个代码在自定义控件用的很多,特别是进度条的自定义控件;

5.scrollTo与scollBy

scrollTo(x,y)表示移动到一个具体的坐标点,而scrollBy(dx,dy)则表示移动的增量为dx、dy。其
中,scollBy最终也是要调用scollTo的。View.java的scollBy和scollTo的源码如下所示:

    /**
     * Move the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the amount of pixels to scroll by horizontally
     * @param y the amount of pixels to scroll by vertically
     */
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }
    
   /**
     * Set the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

scollTo、scollBy移动的是View的内容,如果在ViewGroup中使用,则是移动其所有的子View。
代码如下

class DragView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    private var lastX: Int = 0
    private var lastY: Int = 0

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val x = event.x.toInt()
        val y = event.y.toInt()
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                lastX = x
                lastY = y
            }
            MotionEvent.ACTION_MOVE -> {
                val offsetX = x - lastX
                val offsetY = y - lastX
                (parent as View).scrollBy(-offsetX, -offsetY)
            }
        }
        return true
    }
}

这样就实现了和layout等一样的拖动效果
注:至于这里为什么是负数,其实是参照物不一样导致的,scrollby和scrollto滚动的时候其实是类似于窗口里面的View并没有移动,而是手机屏幕在移动,如果我设置为正数,其实类似手机屏幕往右移动了,view保持不动,这样间接的其实类似于View左移了;

5.Scroller

我们在用scollTo/scollBy方法进行滑动时,这个过程是瞬间完成的,所以用户体验不大好。这里我们可
以使用 Scroller 来实现有过渡效果的滑动,这个过程不是瞬间完成的,而是在一定的时间间隔内完成的。
Scroller本身是不能实现View的滑动的,它需要与View的computeScroll()方法配合才能实现弹性滑动的效
果。

  • computeScroll()
    系统会在绘制View的时候在draw()方法中调用computeScroll()方法。在这个方法中,我们调用父类的scrollTo()方法并通过Scroller来不断获取当前的滚动值,每滑动一小段距离我们就调用invalidate()方法不断地进行重绘,重绘就会调用computeScroll()方法,这样我们通过不断地移动一个小的距离并连贯起来就实现了平滑移动的效果。

写个demo我们将view平滑滚动带屏幕坐标(400,500)的位置,具体代码如下

class DragView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    private var scroller: Scroller

    init {
        scroller = Scroller(context)
    }
    override fun computeScroll() {
        super.computeScroll()
        if (scroller.computeScrollOffset()) {
            (parent as View).scrollTo(scroller.currX, scroller.currY)
            invalidate()
        }
    }

    fun smoothScrollTo(destX: Int, destY: Int) {
        val scrollX = this.scrollX
        val scrollY = this.scrollY
        //偏移量
        val deltaX = destX - scrollX
        val deltaY = destY - scrollY
        scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 2000)
    }

}

调用

 dragView.smoothScrollTo(-400, -500)

实现效果


e.gif

到这里view的滑动就基本差不多了

欢迎大家扫描关注作者公众号,长期推送Android技术干货,感谢大家支持:


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

推荐阅读更多精彩内容