0%

前言

GitHub完整代码

上篇博客简单介绍了属性动画的原理,这篇博客将会以一个简单的实例来运用上之前讲的内容,并对Animator的几个回调方法进行讲解。

目标是自定义一个View,在画布上绘制一个小球,点击屏幕后小球从顶部自由下落,落到底边后反弹,反弹损失一半的能量,也就是说小球只能上升到下落时一半的高度,再重复这个过程直到退出程序。如图:

阅读全文 »

前言

相对于静态的页面,动画往往能更直观地表达所需的信息,在UI开发过程中起着相当大的作用。

Android为我们提供了一系列实现动画效果的方法,PropertyAnimaiton是最常见也是最实用的一种,如同它的名字一样,它的实现方式是通过改变对象的一系列属性值来改变对象的状态, 例如动态地改变绘制的位置就可以实现绘制物体的移动效果,动态地改变对象的显示状态可以实现闪烁效果。

Animator概览

Android提供的实现属性动画的工具是android.animation.Animator这个类,它的使用需要配合animation包下的其他工具类,这个类的功能是什么,我们要如何使用它来实现属性动画呢?

阅读全文 »

draw()方法

经过对View测量布局过程后,下面就到了真正的View绘制的过程了。这个过程从调用根Viewdraw()方法开始:(省略部分代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/

// Step 1, draw the background, if needed
int saveCount;

if (!dirtyOpaque) {
drawBackground(canvas);
}

// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children
dispatchDraw(canvas);

// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}

// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);

// we're done...
return;
}

这段源码来自于View,过程非常清晰,执行了以下的步骤(如果需要):

  1. 27-30行进行判断是否跳过注释中的第2、5步,通常情况跳过
  2. 22-24行,进行背景的绘制
  3. 32行,调用onDraw()方法进行自身的绘制
  4. 35行,调用dispatchDraw()方法,进行子View的绘制(调用子Viewdraw()方法),同时也表明了子View的绘制在自身之后这一顺序
  5. 43行,进行前景的绘制,一般为装饰组件,如滚动条等
阅读全文 »

经过上一篇介绍的measure过程之后,各个View的尺寸信息已经存储在了每个View中,下面是layout过程,layout过程的目的是根据上一步中计算出的尺寸来正确设置各个View及其后代的位置。这个过程首先被调用的是Viewlayout()方法,layout()的方法签名是public void layout(int l, int t, int r, int b) ,四个参数分别为左边界距父View左边界的距离,上边界距父View上边界的距离,右边界距父View左边界的距离,下边界距父View上边界的距离。

1
2
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

changed是用于传递给onLayout()方法的参数,它指示了布局是否被改变。

后面的表达式查看了父View的布局模式是否需要显示边框,如需要,调用的是setOpticalFrame()方法:

1
2
3
4
5
6
7
8
9
10
private boolean setOpticalFrame(int left, int top, int right, int bottom) {
Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;
Insets childInsets = getOpticalInsets();
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}

可以看到这个方法读取了设置的边框值, 把原值加上边框值后还是调用了setFrame()方法。

setFrame()方法通过传入的参数确定了该View最终的位置以及尺寸。

阅读全文 »

上一篇博客简单地介绍了View绘制的生命周期, 从这篇博客开始将会对这个周期中一些有用的过程进行一个详细一些的介绍。这篇的主角就是在构造方法之后调用的measure过程。

为了演示,继承了TextView来实现一个自定义的View。注意这里继承的应该是android.support.v7.widget.AppCompatTextView这个类。同时为了xml文件的正常解析,我们需要实现View的三个构造方法。

1
2
3
4
5
6
7
8
9
10
11
public VView(Context context) {
super(context);
}

public VView(Context context, AttributeSet attrs) {
super(context, attrs);
}

public VView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

再通过完整包名的方法在xml布局文件中创建我们的View就可以直接显示了。

1
2
3
4
5
<com.viseator.viewtest.VView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/Gray"
/>

这里给了TextView一个背景颜色便于后面的观察。

阅读全文 »

为了直观表示整个过程,我制作了一张流程图。注意以下只是整个生命周期中比较常用的方法,并不代表所有的过程。

viewLifeCircle

当一个Activity收到焦点即将要处于激活状态时,将会被要求绘制它的布局,绘制布局之前的过程在这里不涉及,我们从绘制View开始分析。

每个Activity被要求提供一个ViewGroup作为View树的根,也就是我们熟悉的setContentView方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}

@Override
public void setContentView(View view) {
getDelegate().setContentView(view);
}

@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().setContentView(view, params);
}

可以看到setContentView拥有三种形式,可以直接传入View、传入一个layout资源文件,或传入一个View文件和一个用于提供参数的LayoutParams对象。

整个过程将从这个根View开始,并遍历它的子View来逐一绘制,每个ViewGroup承担了要求它的子View进行绘制的责任,每个View承担了绘制自身的责任。并且父View会在子View完成绘制之前进行绘制,同级的View将以它们出现在树中的顺序进行绘制。

阅读全文 »

Java 1.5中引入了泛型的概念以增加代码的安全性与清晰度,同时为了提供对旧代码的兼容性,让旧代码不经过改动也可以在新版本中运行,Java提供了原生态类型(或称原始类型)。但是实际中在新的代码中已经不应该使用原生态类型。

原生态类型的含义是不带任何实际参数的泛型名称,例如Java 1.5后改为泛型实现的List<E>List就是它的原生态类型,与没有引入泛型之前的类型完全一致。

而在虚拟机层面上,是没有泛型这一概念的——所有对象都属于普通类。在编译时,所有的泛型类都会被视为原生态类型。

那么为什么不应该使用原生态类型呢?

如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势。——Effective Java

泛型的目的简单地说就是可以让一些运行时才能发现的错误可以在编译期间就可以被编译器所检测出,运行时出问题的代价与编译期出现问题的代价的差别可想而知。换句话说,泛型是编译器的一种及时发现错误的机制,同时也给用户带来了代码的清晰与简洁的附加好处(不必再写一些复杂而危险并且不直观的强制类型转换)。

下面就进入正题谈谈以List为例时ListList<Object>List<?>的区别。

先下定义:

  • List原生态类型
  • List<Object>参数化的类型,表明List中可以容纳任意类型的对象
  • List<?>无限定通配符类型,表示只能包含某一种未知对象类型
    阅读全文 »