Android UI-移动和滑动效果(一)

概述

android UI 开发中经常要用到滑动效果,这篇博客主要介绍View位置属性和移动的原理。下篇博客会具体介绍如何实现滑动效果。
1.通过动画来实现效果,这主要包括两种,一种是View动画,另外一种是属性动画。
2.通过View提供的ScrollBy和ScrollTo来实现滑动效果。
3.通过translateX和translateY参数来实现滑动效果。
4.其他方式如直接调用layout方法,使用Scroller等。
首先会介绍View的位置参数的相关基础,其中包括了对scroll和translate的原理的讲解。然后会介绍动画的实现方式,最后会用一个小的demo来总结各个实现方式的特点。

Android View的位置参数

要理解View的滑动效果,最好的办法就是理解View各个位置参数的意义,然后了解源码的实现机制从本质上理解View滑动的实现。所以说,我们首先来介绍下布局参数及布局参数所代表的的意义。


image.png

上图显示了在view的实际绘制过程中,一些常见的参数如何确定View的位置。
首先来看一下top,left,buttom,right这四个参数。View的位置主要由它的四个顶点确定。分别对应了top、left、buttom、right这四个属性。这里需要注意的是,四个属性都是对父容器而言的,是一种相对的坐标。那么在如何得到这几个参数呢:View中提供了相应的方法(如下图所示)。View的getTop等方法的解释中提供了非常重要的两点,1、该方法返回的是相对于其父控件的位置2、返回值表示像素值。

    /**
     * Top position of this view relative to its parent.
     *
     * @return The top of this view, in pixels.
     */
    @ViewDebug.CapturedViewProperty
    public final int getTop() {
        return mTop;
    }

我们可以再来看下mTop到底是什么。View的注释中和android开发者官网中已经解释的比较明白了。如下:

 /**
     * The distance in pixels from the top edge of this view's parent
     * to the top edge of this view.
     * {@hide}
     */
    @ViewDebug.ExportedProperty(category = "layout")
    protected int mTop;

视图的几何形状就是矩形的几何形状。视图具有一个位置(以一对水平向左和垂直向上坐标表示)和两个尺寸(以宽度和高度表示)。 位置和尺寸的单位是像素。

可以通过调用方法 getLeft() 和方法 getTop() 来检索视图的位置。前者会返回表示视图的矩形的水平向左(或称 X 轴) 坐标。后者会返回表示视图的矩形的垂直向上(或称 Y 轴)坐标。 这些方法都会返回视图相对于其父项的位置。 例如,如果 getLeft() 返回 20,则意味着视图位于其直接父项左边缘向右 20 个像素处。

此外,系统还提供了几种便捷方法来避免不必要的计算,即 getRight() 和 getBottom()。 这些方法会返回表示视图的矩形的右边缘和下边缘的坐标。 例如,调用 getRight() 类似于进行以下计算:getLeft() + getWidth()。
https://developer.android.com/guide/topics/ui/declaring-layout.html#SizePaddingMargins

已经知道了view中的位置属性,那么能不能设置呢。一般来讲,这个属性是View在绘制过程中动态计算的,虽然View提供了setLeft()等public方法,但是不建议自己去调用。我们直接通过XML去设置就可以了。
然后我们再来解释下translateX和translateY,x和y这四个参数。translateX表示的是View左上角相对于父容器的X轴的偏移量,translateY表示的是View左上角相对于父容器的Y轴的偏移量。乍一看,既然位置都已经用top,left等变量确定了,好像不是特别明白这两个量是用来干什么的。既然这样,那么就先来看下源码是怎么说的吧。如下是getTranslationX的源码:

    /**
     * The horizontal location of this view relative to its {@link #getLeft() left} position.
     * This position is post-layout, in addition to wherever the object's
     * layout placed it.
     *
     * @return The horizontal position of this view relative to its left position, in pixels.
     */
    @ViewDebug.ExportedProperty(category = "drawing")
    public float getTranslationX() {
        return mRenderNode.getTranslationX();
    }

翻译一下:这个view的水平位置,跟getLeft获得的view的left相关。无论对象的布局把view放到了哪边,这个position是layout之后的位置。好像也不是特别明白。再来看下mRenderNode。RenderNode持有了View的一些属性,特别是持有了View内容的绘制呈现表。

 /**
     * RenderNode holding View properties, potentially holding a DisplayList of View content.
     * <p>
     * When non-null and valid, this is expected to contain an up-to-date copy
     * of the View content. Its DisplayList content is cleared on temporary detach and reset on
     * cleanup.
     */
    final RenderNode mRenderNode;

最后x和y代表了什么呢,这个比较简单,只要看下源码就非常清楚了。x表示view的视图位置。

    /**
     * The visual x position of this view, in pixels. This is equivalent to the
     * {@link #setTranslationX(float) translationX} property plus the current
     * {@link #getLeft() left} property.
     *
     * @return The visual x position of this view, in pixels.
     */
    @ViewDebug.ExportedProperty(category = "drawing")
    public float getX() {
        return mLeft + getTranslationX();
    }

综上,我们就可以明白,top、left虽然定义了view的位置信息,但是最后视图呈现出来的位置是在这基础上加上translated 的偏移量。所以在做滑动处理的时候,我们既可以改变view位置的值,也可以设置偏移量的值。

最后来看一下ScrollX和ScrollY的属性值。照旧,还是先来看源码是怎么实现的。

    /**
     * The offset, in pixels, by which the content of this view is scrolled
     * horizontally.
     * {@hide}
     */
    @ViewDebug.ExportedProperty(category = "scrolling")
    protected int mScrollX;

ScrollBy是用ScrollTo实现的。ScrollBy的意思是移动多少距离,而scrollTo的意识是移动到什么位置。然后会在onScrollChanged这个方法中去执行。

    /**
     * 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();
            }
        }
    }

    /**
     * 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);
    }

需要注意的是,ScrollX和ScrollY只能改变View的内容而不能改变View在布局中的位置,Scroll改变的是像素值,而且scroll的方向与x轴和y轴的方向相反。

动画实现

动画实现由两种,一种是View动画,一种是属性动画。先说View动画,这种实现方式非常简单,只要给View添加一个TranslateAnimation就可以了,但是这种方式有一个致命的缺陷,那就是View其实还是在原来的地方,点击事件并不能随着view的视图位置的移动而改变。
实现方式如下:

TranslateAnimation animator = new TranslateAnimation(0, 50, 0, 50);
animator.setFillAfter(true);
view_1.startAnimation(animator);

在TransleAnimation类中发现了这样一句代码:t.getMatrix().setTranslate(dx, dy); 推测View动画是通过Matrix变换来达到动画效果的,所以不会改变任何位置的属性。

第二种是属性动画,实现起来也非常简单:

ObjectAnimator.ofFloat(view_1, "TranslationX", 0, 100).setDuration(100).start();

可以推测,这是通过反射方式拿到TranslationX相关的set方法,然后对TranslationX属性进行设置。这种方式和直接通过TranslationX、Y的原理是一样的,只是多增加了动画的效果。

其他方式

其实在view的体系中(如下图),每个view都会有一个mLayoutParams对象。这个对象会在view父控件onMeasure的时候产生作用,计算出childWidthMeasureSpec和childHeightMeasureSpec,然后子View measure时计算出子view的位置属性。


image.png

设置时,通过View.getLayoutParams();方法获取相应的Params对象,然后设置Margin和Padding属性,同样会有移动的效果。

FrameLayout.MarginLayoutParams params = (FrameLayout.MarginLayoutParams) view_1.getLayoutParams();
params.leftMargin = 100;
view_1.setLayoutParams(params);
view_1.requestLayout();

Android View参数改变的示例

上面说了这个多,其实并没有对这几种方式有直观的认识,下面我们来用一个例子说明一下。我们定义三个有子View的FrameLayout,然后观察不同的方式的行为。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.chengnan.windowtest.MainActivity">

    <FrameLayout
        android:id="@+id/view_1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true">

        <View
            android:id="@+id/view_1_inner"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="@color/colorAccent_1" />

    </FrameLayout>

    <FrameLayout
        android:id="@+id/view_2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true">

        <View
            android:id="@+id/view_2_inner"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="@color/colorPrimary_1" />
    </FrameLayout>

    <FrameLayout
        android:id="@+id/view_3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true">

        <View
            android:id="@+id/view_3_inner"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="@color/colorPrimaryDark_1" />
    </FrameLayout>

</RelativeLayout>

MainActivity中的代码

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        FrameLayout view_1 = (FrameLayout) findViewById(R.id.view_1);
        FrameLayout view_2 = (FrameLayout) findViewById(R.id.view_2);
        FrameLayout view_3 = (FrameLayout) findViewById(R.id.view_3);
        View view_1_inner = findViewById(R.id.view_1_inner);
        View view_2_inner = findViewById(R.id.view_2_inner);
        View view_3_inner = findViewById(R.id.view_3_inner);

        view_1.setTranslationX(100);
        view_1.setTranslationY(100);

        view_2.scrollBy(100, 100);

        //对view_3设置属性动画
        ObjectAnimator.ofFloat(view_3, "translationX", 0, -100).setDuration(100).start();

        //对view_3设置View动画
        TranslateAnimation animator = new TranslateAnimation(0, -100, 0, 100);
        animator.setFillAfter(true);
        view_3.startAnimation(animator);
    }

效果:


image.png

从效果中可以看到,Scroll确实是移动了View中的内容(因为子View发生了偏移),还可以发现Scroll方式不会原来的布局有影响,因为移动,一部分内容超出了父布局的范围所以没有绘制出来。
接下去,演示子View的滑动效果

view_1_inner.setTranslationX(100);
view_1_inner.setTranslationY(100);

//view_2.scrollBy(100, 100);

//对view_3_inner设置属性动画
ObjectAnimator.ofFloat(view_3_inner, "translationX", 0, -100).setDuration(100).start();

//对view_3_inner设置View动画
TranslateAnimation animator = new TranslateAnimation(0, -100, 0, 100);
animator.setFillAfter(true);
view_3.startAnimation(animator);
image.png

这里对View_2不做任何处理,可以发现,Translation也不会对原来的父容器和自己的布局参数有任何影响。移动后同样有一部分无法显示。
而view_3_inner向左移动了100,可以看到缺少了1/3,这和直接使用TranslationX的效果是一样的。但是图形整个向左位移了,这说明TranslateAnimation 的动画绘制不会受到父容器和自己布局属性的影响。

总结

Scroll是对内容的移动变换,改变的是mScrollX和mScrollY属性,不会对原布局造成影响。
Translation是对控件位置的变换,改变的是translationX和translationY属性,不会对原父布局造成影响。
属性动画同Translation,通过反射的方式获得set和get方法进行设置。
View动画是绘制时的矩阵变化,不会对布局造成影响,同时绘制区域也不受原布局的影响。
LayoutParams的margin是对控件的left、top等属性的改变,会重新测量整体的布局,会对父控件产生影响。
关于View动画点击区域和点击效果这里便不再演示。差不多介绍了View的位置属性和动画原理,下篇博客将介绍具体的动画效果的实现。

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

推荐阅读更多精彩内容