概述
android UI 开发中经常要用到滑动效果,这篇博客主要介绍View位置属性和移动的原理。下篇博客会具体介绍如何实现滑动效果。
1.通过动画来实现效果,这主要包括两种,一种是View动画,另外一种是属性动画。
2.通过View提供的ScrollBy和ScrollTo来实现滑动效果。
3.通过translateX和translateY参数来实现滑动效果。
4.其他方式如直接调用layout方法,使用Scroller等。
首先会介绍View的位置参数的相关基础,其中包括了对scroll和translate的原理的讲解。然后会介绍动画的实现方式,最后会用一个小的demo来总结各个实现方式的特点。
Android View的位置参数
要理解View的滑动效果,最好的办法就是理解View各个位置参数的意义,然后了解源码的实现机制从本质上理解View滑动的实现。所以说,我们首先来介绍下布局参数及布局参数所代表的的意义。
上图显示了在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的位置属性。
设置时,通过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);
}
效果:
从效果中可以看到,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);
这里对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的位置属性和动画原理,下篇博客将介绍具体的动画效果的实现。