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滑动的实现。所以说,我们首先来介绍下布局参数及布局参数所代表的的意义。


Android UI-移动和滑动效果(一)_第1张图片
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.
     * 

* 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的位置属性。


Android UI-移动和滑动效果(一)_第2张图片
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,然后观察不同的方式的行为。




    

        

    

    

        
    

    

        
    


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

效果:


Android UI-移动和滑动效果(一)_第3张图片
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);
Android UI-移动和滑动效果(一)_第4张图片
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的位置属性和动画原理,下篇博客将介绍具体的动画效果的实现。

你可能感兴趣的:(Android UI-移动和滑动效果(一))