使用Transition FrameWork实现有意义的转场动画(译)

原文

Android Transition Framework可以实现三种效果:

  1. 不同Activity之间切换时,Activityc的内容(contentView)转场动画
  2. 不同Activity之间切换时,如果使用了Shared Element动画,也可以使用Transition FrameWork来实现不同的过渡动画效果
  3. 同一个Activity内View变化的过渡动画(Scene)

1. Activity之间切换的过渡动画

通过这种方法可以使activity切换时,他们的布局内容有过度动画

当从Activity A切换到Activity B的时候,Activity布局的内容会按照预先定义好的动画来执行过渡动画。在android.transition包中,已经有三种现成的动画可以用:Explode,Slide和Fade。所有这些过渡都会跟踪activity布局中可见的目标Views,驱动这些Views按照过渡的规则产生响应的动画效果。

Explode Explode Explode

<– more –>

你可以在xml中创建这些过渡效果,也可以通过代码来创建。对于Fade过渡效果来说,它看起来是这样子的:

xml中创建

过渡效果定义在xml中,目录是res/transition

res/transition/activity_fade.xml

1
2
3
<?xml version="1.0" encoding="utf-8"?>
<fade xmlns:android="http://schemas.android.com/apk/res/"
android:duration="1000"/>

res/transition/activity_slide.xml

1
2
3
<?xml version="1.0" encoding="utf-8"?>
<slide xmlns:android="http://schemas.android.com/apk/res/"
android:duration="1000"/>

要使用这些xml’中定义的过渡动画,你需要使用TransitionInflater来实例化它们。

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transition);
setupWindowAnimations();
}

private void setupWindowAnimations() {
Slide slide = TransitionInflater.from(this).inflateTransition(R.transition.activity_slide);
getWindow().setExitTransition(slide);
}

TransitionActivity.java

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transition);
setupWindowAnimations();
}

private void setupWindowAnimations() {
Fade fade = TransitionInflater.from(this).inflateTransition(R.transition.activity_fade);
getWindow().setEnterTransition(fade);
}

在代码中创建

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transition);
setupWindowAnimations();
}

private void setupWindowAnimations() {
Slide slide = new Slide();
slide.setDuration(1000);
getWindow().setExitTransition(slide);
}

TransitionActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transition);
setupWindowAnimations();
}

private void setupWindowAnimations() {
Fade fade = new Fade();
fade.setDuration(1000);
getWindow().setEnterTransition(fade);
}

不管哪种创建方法都会产生如下的效果:

transition_fade

那么这里面一步一步的到底发生了什么:

  1. Activity A 启动 Activity B

  2. Transition Framework 发现 A 中定义了Exit Transition (slide) 然后就会对它的所有可见的View使用这个过渡动画.

  3. Transition Framework 发现 B 中定义了Enter Transition (fade) 然后机会对它所有可见的Views使用这个过渡动画.
  4. On Back Pressed(按返回键) Transition Framework 会执行把 Enter and Exit过渡动画反过来执行(但是如果定义了 returnTransitionreenterTransition,那么就会执行这些定义的动画)

译注:

  • Exit Transition:可以理解为activity进入后台的过渡动画
  • Enter Transition:可以理解为创建activity并显示时的过渡动画
  • Return Transition:可以理解为销毁activity时的过渡动画
  • Reenter Transition:可以理解为activity从后台进入前台时的过渡动画
  • 要使这些过渡动画生效,我们需要调用startActivity(intent,bundle)方法来启动activity。bundle需要通过ActivityOptionsCompat.makeSceneTransitionAnimation().toBundle()的方式来生成

ReturnTransition & ReenterTransition

Return and Reenter Transitions是与进入和退出动画相对应的.

  • EnterTransition <–> ReturnTransition
  • ExitTransition <–> ReenterTransition

如果Return or Reenter没有创建, Android 会把Enter and Exit Transitions反过来执行. 但是如果你创建了Return or Reenter,那Android就会执行你创建的动画,并且这些动画可以不同.

b back a

我们可以修改下前面Fade的例子,给 TransitionActivity创建 ReturnTransition。这里我们就拿Slide过渡效果来举例子。这样,如果我们从B返回到A的时候,B就会执行一个Slide的过渡效果。

TransitionActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transition);
setupWindowAnimations();
}

private void setupWindowAnimations() {
Fade fade = new Fade();
fade.setDuration(1000);
getWindow().setEnterTransition(fade);

Slide slide = new Slide();
slide.setDuration(1000);
getWindow().setReturnTransition(slide);
}

可以看到,如果没有创建Return Transition,退出的时候会执行Enter Transtion相反的动画。如果创建了Return Transition,那么就会执行这个创建的动画效果。

没有Return Transition 有Return Transition
Enter: Fade In Enter: Fade In
Exit: Fade Out Exit: Slide out
transition_fade transition_fade2

2. Activity之间共享元素(Share Elements)

这里的思想就是通过动画的形式将两个不同布局中的两个不同的View联系起来。
然后Transition framework就会在用户从一个View切换到另一个View的时候给用户展现一些必要的动画。
但你要记住:发生动画的View并不是从一个布局中移动到另一个布局。他们是两个独立的View。

A Start B with shared

a) 设置Window Content Transition属性

你需要在app的 styles.xml中进行设置.[译]我没有设置也没问题

values/styles.xml

1
2
3
4
5
<style name="MaterialAnimations" parent="@style/Theme.AppCompat.Light.NoActionBar">
...
<item name="android:windowContentTransitions">true</item
...
</style>

如果你愿意的话,你也可以给整个app设置一个默认的转场动画和共享元素动画。

1
2
3
4
5
6
7
8
9
10
11
<style name="MaterialAnimations" parent="@style/Theme.AppCompat.Light.NoActionBar">
...
<!-- specify enter and exit transitions -->
<item name="android:windowEnterTransition">@transition/explode</item>
<item name="android:windowExitTransition">@transition/explode</item>

<!-- specify shared element transitions -->
<item name="android:windowSharedElementEnterTransition">@transition/changebounds</item>
<item name="android:windowSharedElementExitTransition">@transition/changebounds</item>
...
</style>

b)设置相同的transition name

为了使共享元素动画生效,你需要给共享元素的两个View设置相同的android:transitionName属性值。不过他们的id和其他属性可以不同。

layout/activity_a.xml

1
2
3
4
5
<ImageView
android:id="@+id/small_blue_icon"
style="@style/MaterialAnimations.Icon.Small"
android:src="@drawable/circle"
android:transitionName="@string/blue_name" />

layout/activity_b.xml

1
2
3
4
5
<ImageView
android:id="@+id/big_blue_icon"
style="@style/MaterialAnimations.Icon.Big"
android:src="@drawable/circle"
android:transitionName="@string/blue_name" />

c) 用共享元素来启动activity

使用ActivityOptions.makeSceneTransitionAnimation() 方法指定要共享元素的View和android:transitionName属性的值

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13

blueIconImageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent i = new Intent(MainActivity.this, SharedElementActivity.class);

View sharedView = blueIconImageView;
String transitionName = getString(R.string.blue_name);

ActivityOptions transitionActivityOptions = ActivityOptions.makeSceneTransitionAnimation(MainActivity.this, sharedView, transitionName);
startActivity(i, transitionActivityOptions.toBundle());
}
});

这样就可以有下面漂亮的过渡效果了:

a to b with shared element

可以看到, Transition framework 创建并执行了一个动画。动画的视觉效果就是一个View从一个activity移动到另一个activity中并伴随着形状的变化。

在fragment之间实现Shared elements过渡动画

这个activity中的做法几乎是一样的。

a)b)完全一样, 只有c)有点区别

a) 设置Window Content Transition属性

values/styles.xml

1
2
3
4
5
<style name="MaterialAnimations" parent="@style/Theme.AppCompat.Light.NoActionBar">
...
<item name="android:windowContentTransitions">true</item>
...
</style>

b) 设置相同的transition name

layout/fragment_a.xml

1
2
3
4
5
<ImageView
android:id="@+id/small_blue_icon"
style="@style/MaterialAnimations.Icon.Small"
android:src="@drawable/circle"
android:transitionName="@string/blue_name" />

layout/fragment_b.xml

1
2
3
4
5
<ImageView
android:id="@+id/big_blue_icon"
style="@style/MaterialAnimations.Icon.Big"
android:src="@drawable/circle"
android:transitionName="@string/blue_name" />

c) 启动带有共享元素的fragment

在你使用FragmentTransaction启动fragment的时候,你需要同时带上共享元素过渡动画的先关信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FragmentB fragmentB = FragmentB.newInstance(sample);

// Defines enter transition for all fragment views
Slide slideTransition = new Slide(Gravity.RIGHT);
slideTransition.setDuration(1000);
sharedElementFragment2.setEnterTransition(slideTransition);

// Defines enter transition only for shared element
ChangeBounds changeBoundsTransition = TransitionInflater.from(this).inflateTransition(R.transition.change_bounds);
fragmentB.setSharedElementEnterTransition(changeBoundsTransition);

getFragmentManager().beginTransaction()
.replace(R.id.content, fragmentB)
.addSharedElement(blueView, getString(R.string.blue_name))
.commit();

最终的效果就是这样的:

shared_element_no_overlap

允许过渡效果之间的重叠

You can define if enter and exit transitions can overlap each other.
你可以设置一个activity的退出效果和另一个activity的进入效果产生重叠部分。

Android文档)这么说的:

当设置为true,enter transition会立马执行>
当设置为false,enter transition会等到退出exit transition结束后再执行.

这对Fragments和Activities的共享元素过渡效果都是有用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FragmentB fragmentB = FragmentB.newInstance(sample);

// Defines enter transition for all fragment views
Slide slideTransition = new Slide(Gravity.RIGHT);
slideTransition.setDuration(1000);
sharedElementFragment2.setEnterTransition(slideTransition);

// Defines enter transition only for shared element
ChangeBounds changeBoundsTransition = TransitionInflater.from(this).inflateTransition(R.transition.change_bounds);
fragmentB.setSharedElementEnterTransition(changeBoundsTransition);

// Prevent transitions for overlapping
fragmentB.setAllowEnterTransitionOverlap(overlap);
fragmentB.setAllowReturnTransitionOverlap(overlap);

getFragmentManager().beginTransaction()
.replace(R.id.content, fragmentB)
.addSharedElement(blueView, getString(R.string.blue_name))
.commit();

可以看到,效果还是挺明显的:

Overlap True Overlap False
Fragment_2 出现在Fragment_1的上面 Fragment_2 等到Fragment_1消失后才出现
shared_element_overlap shared_element_no_overlap

3. 布局元素动画

Scenes

也可以用来驱动一个activity中的布局发生变化时,让其中的View产生过渡动画。

过渡效果发生在场景之间。场景就是一个定义了静态UI的普通布局。Transition Framework可以在两个场景之间添加切换过渡动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
scene1 = Scene.getSceneForLayout(sceneRoot, R.layout.activity_animations_scene1, this);
scene2 = Scene.getSceneForLayout(sceneRoot, R.layout.activity_animations_scene2, this);
scene3 = Scene.getSceneForLayout(sceneRoot, R.layout.activity_animations_scene3, this);
scene4 = Scene.getSceneForLayout(sceneRoot, R.layout.activity_animations_scene4, this);

(...)

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button1:
TransitionManager.go(scene1, new ChangeBounds());
break;
case R.id.button2:
TransitionManager.go(scene2, TransitionInflater.from(this).inflateTransition(R.transition.slide_and_changebounds));
break;
case R.id.button3:
TransitionManager.go(scene3, TransitionInflater.from(this).inflateTransition(R.transition.slide_and_changebounds_sequential));
break;
case R.id.button4:
TransitionManager.go(scene4, TransitionInflater.from(this).inflateTransition(R.transition.slide_and_changebounds_sequential_with_interpolators));
break;
}
}

上面的代码会在同一个activity中的4个场景切换时产生过渡动画。每一次切花的动画都是不一样的。

Transition Framework会考虑当前场景内所有可见的View并计算需出需要的动画来安排两个场景之间的view的位置。

scenes_anim

布局的改变

也可以用来给布局属性的变化加上过渡效果。你只需要做你想要的改变然后其他的就交给Transition Framework,它会为你添加动画。

a) 开启演示过渡效果

下面这行代码告诉framework我们将要对UI进行一些改变,请你给我加上过渡效果。

1
TransitionManager.beginDelayedTransition(sceneRoot);

b) 改变布局参数

1
2
3
ViewGroup.LayoutParams params = greenIconView.getLayoutParams();
params.width = 200;
greenIconView.setLayoutParams(params);

改变View的宽度属性让他变小,这会触发layoutMeasure。这个点上,Transition framework会记录开始和结束时的相关值,并给这个变化加上过渡效果。

view layout animation

4. (彩蛋) 共享元素(share elements)+圆形展现(Circular Reveal)

圆形展现仅仅一个现实和隐藏一组view的动画而已。API21+可以通过ViewAnimationUtils来使用它。

Circular Reveal动画可以结合共享元素过渡效果来创建一些有意义的动画来告诉用户app在发生中什么。

reveal_shared_anim

这又是怎么回事呢:

  • 橙色的圆是发生在MainActivityRevealActivity之间的共享元素动画。
  • RevealActivity中有一个共享元素动画结束的监听器。当动画结束时它做了两件事:
    • 给Toolbar执行一个Circular Reveal动画
    • 使用ViewPropertyAnimatorRevealActivity的其他View执行一个放大的动画

监听共享元素动画的结束

1
2
3
4
5
6
7
8
9
10
11
12
Transition transition = TransitionInflater.from(this).inflateTransition(R.transition.changebounds_with_arcmotion);
getWindow().setSharedElementEnterTransition(transition);
transition.addListener(new Transition.TransitionListener() {
@Override
public void onTransitionEnd(Transition transition) {
animateRevealShow(toolbar);
animateButtonsIn();
}

(...)

});

显示Toolbar

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
private void animateRevealShow(View viewRoot) {
int cx = (viewRoot.getLeft() + viewRoot.getRight()) / 2;
int cy = (viewRoot.getTop() + viewRoot.getBottom()) / 2;
int finalRadius = Math.max(viewRoot.getWidth(), viewRoot.getHeight());

Animator anim = ViewAnimationUtils.createCircularReveal(viewRoot, cx, cy, 0, finalRadius);
viewRoot.setVisibility(View.VISIBLE);
anim.setDuration(1000);
anim.setInterpolator(new AccelerateInterpolator());
anim.start();
}
```

> 放大activity的内部view

```java
private void animateButtonsIn() {

for (int i = 0; i < bgViewGroup.getChildCount(); i++) {
View child = bgViewGroup.getChildAt(i);
child.animate()
.setStartDelay(100 + i * DELAY)
.setInterpolator(interpolator)
.alpha(1)
.scaleX(1)
.scaleY(1);
}
}

更多circular reveal动画

有很多种方式来实现一个view的显示动画,但重要的是要让这些动画有意义,它可以告诉用户app中正在发生着什么。

从目标View的中间产生一个Circular Reveal动画

reveal_green

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
int cx = (viewRoot.getLeft() + viewRoot.getRight()) / 2;
int cy = viewRoot.getTop();
int finalRadius = Math.max(viewRoot.getWidth(), viewRoot.getHeight());

Animator anim = ViewAnimationUtils.createCircularReveal(viewRoot, cx, cy, 0, finalRadius);
viewRoot.setBackgroundColor(color);
anim.start();
```

#### 从目标View的顶部产生一个Circular Reveal动画并且结合了其他的动画

![reveal_blue]

```java
int cx = (viewRoot.getLeft() + viewRoot.getRight()) / 2;
int cy = (viewRoot.getTop() + viewRoot.getBottom()) / 2;
int finalRadius = Math.max(viewRoot.getWidth(), viewRoot.getHeight());

Animator anim = ViewAnimationUtils.createCircularReveal(viewRoot, cx, cy, 0, finalRadius);
viewRoot.setBackgroundColor(color);
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
animateButtonsIn();
}
});
anim.start();
```


#### 从触摸点产生一个Circular Reveal动画

![reveal_yellow]

```java
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
if (view.getId() == R.id.square_yellow) {
revealFromCoordinates(motionEvent.getRawX(), motionEvent.getRawY());
}
}
return false;
}
private Animator animateRevealColorFromCoordinates(int x, int y) {
    float finalRadius = (float) Math.hypot(viewRoot.getWidth(), viewRoot.getHeight());

    Animator anim = ViewAnimationUtils.createCircularReveal(viewRoot, x, y, 0, finalRadius);
    viewRoot.setBackgroundColor(color);
    anim.start();
}

动画和展现

reveal_red

Transition transition = TransitionInflater.from(this).inflateTransition(R.transition.changebounds_with_arcmotion);
transition.addListener(new Transition.TransitionListener() {
    @Override
    public void onTransitionEnd(Transition transition) {
        animateRevealColor(bgViewGroup, R.color.red);
    }
    (...)

});
TransitionManager.beginDelayedTransition(bgViewGroup, transition);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
btnRed.setLayoutParams(layoutParams);

Sample source code

https://github.com/lgvalle/Material-Animations

More information

VectorDrawable怎么玩

摘要

从5.0(API等级21)开始,android开始支持矢量图了。关于什么是矢量图以及矢量图有什么优缺点不在本文的涉及范围之内,具体可以参考矢量图百科。不过这里要提一下它的优点:

  • 保存最少的信息,文件大小比位图要小,并且文件大小与物体的大小无关
  • 任意放大矢量图形,不会丢失细节或影响清晰度,因为矢量图形是与分辨率无关的。

怎么使用Toolbar之给Toolbar加上动画

摘要

通过上一篇文章我们初次认识了下Toolbar。聊了下怎么把Toolbar集成到项目中和Toolbar的基本设置这两个问题。接下来聊聊怎么给Toolbar加上一些交互效果,类似Play商店上的那些效果。

效果一:使Toolbar随着内容区域的滚动而隐藏和显示

我们知道手机屏幕的大小时候限的,有时候我们为了显示更多的内容需要隐藏掉一些不相关的内容,比如Toolbar。以前我们可能会使用属性动画或者通过view.animate().translationXX()这个便捷的方法来实现这些效果。现在就不用这么麻烦了,只需要在xml中添加两行代码就可以了。

为了实现上述的效果,这里需要引入两个新的控件:CoordinatorLayoutAppBarLayout,这两个控件均位于design兼容包中。所以你需要在module的build.gradle依赖中加入下面一行代码。

1
compile 'com.android.support:design:23.1.0'
  • AppBarLayout:本质上是一个垂直的线性布局。但是他实现了材料设计中app bar的滚动手势的特性。而为了让这些特性发挥效果,你必须把AppBarLayout作为CoordinatorLayout的一个直接子控件来使用。并且,你还需要为AppBarLayout设置一个支持NestedScroll的兄弟控件。这样父控件CoordinateLayout就知道什么时候来响应滚动事件了 它的子控件可以通过setScrollFlags(int)或者app:layout_scrollFlags的方式来为自己指定滚动行为。可选的行为有:SCROLL_FLAG_ENTER_ALWAYSSCROLL_FLAG_ENTER_ALWAYS_COLLAPSED
    SCROLL_FLAG_EXIT_UNTIL_COLLAPSEDSCROLL_FLAG_SCROLLSCROLL_FLAG_SNAP
  • CoordinateLayout:本质上是一个增强版的FrameLayout。一般作为一个容器来使用,这样可以让它的子控件实现一些交互效果。可以通过给子控件指定不同的Behaviors来实现不同的交互效果。

扯了这么多好像也没啥感觉,感觉还真是“Talk is cheap. Show me the code.”呢。那下来就撸代码,看效果吧。

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
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.demo.activity.MainActivity">


<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">


<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/AppTheme.PopupOverlay"/>


</android.support.design.widget.AppBarLayout>

<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />


</android.support.design.widget.CoordinatorLayout>

上面的布局中有两个地方需要注意:1.Toolbar的app:layout_scrollFlags="scroll|enterAlways"属性 2.RecyclerView的app:layout_behavior="@string/appbar_scrolling_view_behavior"属性。这两个地方就是上文中加粗部分的提到的注意点。同时,注意下整个布局的结构:CoordinateLayout作为跟布局,内部分别放置了一个AppBarLayout和RecyclerView。Toolbar作为AppBarLayout的子控件而存在。
其实,就改这么点地方就可以了。想要的效果已经有了。不信给你看看效果图。

效果二:多个控件协同作战(其实没想出来怎么描述这种效果)

这种效果我还不好描述,直接上图一看你就明白。一图顶千言啊!

从上面的效果图中可以看到,随着内容区域的滑动顶部的图片和Toolbar也会做出相应的动作。图片会收缩和展开,Toolbar的背景在透明和蓝色之间变化。要实现这种类似Play商店的效果,还需要引入一个新的控件:CollapsingToolbarLayout。该控件同样位于design兼容包中。

CollapsingToolbarLayout是对Toolbar的包装,它实现了可收缩的app bar效果,而且被设计为AppBarLayout的直接子类来使用的。

引入CollapsingToolbarLayout控件后,我的布局变成了这样子。

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
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.demo.activity.MainActivity">


<android.support.design.widget.AppBarLayout

android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">


<android.support.design.widget.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="240dp"
app:collapsedTitleGravity="center"
app:contentScrim="@color/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed">


<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="@drawable/cheese_1"
app:layout_collapseMode="parallax" />


<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/AppTheme.PopupOverlay" />

</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>

<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />


</android.support.design.widget.CoordinatorLayout>

可以看到,Toolbar被CollapsingToolbarLayout包裹起来了,并且给Toolbar添加了一个ImageView的兄弟控件。同时在CollapsingToolbarLayout的子控件上分别设置了app:layout_collapseMode属性(姑且就叫收缩模式吧),该属性有两种值:parallax和pin。parallax属性值会让的CollapsingToolbarLayout子控件在滚动时呈现出视差效果,而pin则会让它的子控件保持不动。从上面设置的值对比着效果图看,应该就比较清晰了。

CollapsingToolbarLayout有很多属性可以控制滚动过程中文字的滚动轨迹,文字样式,文字的位置以及它的子控件之间的配合方式。比如collapsedTitleGravity可以设置收缩结束时title的对其方式,上例中使用了居中对齐,所以title最终显示在了toolbar的中间,默认是靠左显示的。

其实之前提到的这些新控件配合起来可以实现的效果还是挺多的,这里面有一个起到重要角色的对象:Behavior。它定义了CoordinateLayout的子控件之间的交互行为,如果想实现一些自定义的交互效果,那么就需要自己去实现一些Behavior了。关于Behavior的自定义目前还不太了解,以后有机会的话也会研究下。

这篇就说这么多吧,计划下一篇把CollapsingToolbarLayout的那些属性总结下,然后再搞下Android抽屉的用法。

怎么使用Toolbar之集成与属性配置

摘要

Toolbar面世也有些年头了,但线上项目中一直没有机会使用。想着再不用就太对不起这么好的控件了,所以最近抽空研究了下Toolbar。写下这篇文章,做个总结。

Toolbar的问世是为了替换以前的ActionBar,它比ActionBar的使用更加灵活方便,它可以放在布局中的任何位置。不过一般都是把Toolbar放在布局的顶部来使用的。下面我就一步步学习下怎么使用Toolbar吧。

Handler机制分析

最近翻译了一片关于Context泄露的文章,其中提到了Handler的一些知识点。想想当初自己研究这块的时候也是看着源码一点点抠出来的。只不过那时候没有做个总结,现在正好借着这个机会把这块知识点总结下,也算是做个备注了。

我们知道Android应用启动的时候会执行ActivityThread类的main方法,详情请参见罗升阳的这篇文章。main方法中会为主线程创建一个Looper对象,这个Looper中维护了一个消息队列MessageQueen。Looper会不断的从消息队列中取消息,然后交给消息的target对象(即Handler)去处理这个消息。如果队列中没有消息,那么Looper就会进入等待状态直到队列中有新消息。大概的过程就是这样,下面我们通过源码来了解下整个过程。

学习EventBus之发布事件

上篇文章中,我们从源码里了解了EventBus的注册机制。这篇文章我们接着上篇,把另一半——发布来了解一下。

我们一般可以通过eventBus.getDefault().post(event)的方式来发布一个事件,然后订阅了这个类型事件的类就会接收到这个事件并进行相应的处理。那么,我们就从post(event)下手,看看post的原理是什么样子的。

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
/** Posts the given event to the event bus. */
public void post(Object event) {
//获取当前发布线程一个线程局部变量,这个变量存储了
//发布事件,事件订阅者以及发布是否是主线程等信息
PostingThreadState postingState = currentPostingThreadState.get();
//当前线程的事件队列
List<Object> eventQueue = postingState.eventQueue;
//将新发布的事件加入队列
eventQueue.add(event);

if (!postingState.isPosting) {
//当前发布的线程是否在主线程
postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper();
//更改发布状态
postingState.isPosting = true;
if (postingState.canceled) {
//这个状态会在发布成功后重置为false
throw new EventBusException("Internal error. Abort state was not reset");
}
try {
while (!eventQueue.isEmpty()) {//事件队列不为空 推送队列头的事件 并从队列中移除该事件
postSingleEvent(eventQueue.remove(0), postingState);
}
} finally {
//结束发布 重置推送状态参数
postingState.isPosting = false;
postingState.isMainThread = false;
}
}
}

可以看到在当前线程里有个线程局部变量PostingThreadState,

LayoutInflater的传参问题

Android开发中,我们会经常需要把一个xml解析成一个View。最常见的就是在Adapter的getView方法中通过LayoutInflater把一个布局文件解析成一个View。但是,这里经常会出现一些问题,比如我在item布局文件的根布局中设置的属性经常会没效果等等。其实,在我们在Activity的onCreate方法中调用Activity的setContentView方法给Activity设置布局的时候,底层也是通过LayoutInflater来将传入的布局解析成View然后在添加到界面上的。所以,为了搞清楚LayoutInflater的工作原理,我翻了翻源码,基本上把问题搞明白了。

先来看一个问题:在给ListView填充item布局时,

Gradle小知识#3:任务的顺序

我发现在使用Gradle的过程中遇到的很多问题都跟任务的顺序有关系,不管是已经存在的任务还是我自定义的任务。很显然,如果任务能在正确的时间执行的话,构建任务就能更好的工作了。

所以让我们深入了解下怎么更改任务的执行上顺序这个问题。

dependsOn

我觉得让某个任务在另一个任务之后执行的最简单的方法就是使用dependsOn)方法。

假设有一个任务A,现在我们要添加一个任务B并且任务B只有在任务A执行后它才会执行:

先来定义两个任务,任务AB

1
2
task A << {println 'Hello from A'}
task B << {println 'Hello from B'}

然后你要做的就是告诉Gradle任务B依赖于任务A

1
B.dependsOn A

这样不管什么时候我执行任务B,Gradle也会执行任务A

Gradle小知识#2:学学语法

在第一部分,我们聊了下Gradle中的任务以及构建过程中的不同阶段。但是,在我发布上篇文章之后我意识到在继续深入Gradle之前,我们最好了解下Gradle的语法。以免被复杂的build.gradle脚本吓到。所以这篇我们来聊聊Gradle的语法。

语法

Gradle构建脚本是使用Groovy语言写的。所以在继续深入之前我想说几个Groovy中比较重要的概念。Groovy的语法跟Java语法很类似,所以希望你在理解上不会有大问题。

如果你对Groovy比较熟悉,那你可以跳过这部分。

要理解Gradle脚本,你需要明白Groovy中很重要的一个概念——闭包。

闭包

要理解Gradle就必须搞明白闭包这个重要的概念。闭包是一个独立的代码块,它可以接收参数、可以有返回值,而且还可以赋值给变量。有点像Callable接口、Future和方法指针的混合体,随便你怎么叫。

大体来说就是个代码块,当你调用它的时候它才会执行,而非创建时就执行。下面是一个简单的闭包的例子:

Gradle小知识#1:tasks

从这篇博文开始我打算开启关于Gradle相关知识的一些列博文。现在想想,如果我刚开始接触Gradle的时候知道这些知识的话那该多好啊。

今天我们来聊聊Gradle的任务,尤其是任务的配置和执行部分。因为这块知识对很多读者来说还不是很清楚,所以通过一个真实的例子来说明下就再好不过了。大体上(抱歉有点超前)我们是想要弄清楚下面这三个例子到底有啥区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
task myTask {
println "Hello, World!"
}

task myTask {
doLast {
println "Hello, World!"
}
}

task myTask << {
println "Hello, World!"
}

我的目的是创建