手动实现布局Transitions动画-第二部分

布局切换动画在Material design中是一个重要的方面,因为它们能够指明应用的工作流程,并且能够将UI上的可视化元素绑定在一起作为用户的导航。两个重要的工具可以实现这种效果,分别为Activity转场动画和布局动画(Layout Transitions)。然后布局动画需要在API 19及其之后才支持。在这一系列文章中,我们会学习到即使在无法调用transitions APIs时如何实现很好的转场动画。

布局切换框架引入了代表特定布局状态的Scenes概念,也就是场景。我们会定义两个分离的布局来模仿这些,其中一个代表默认的视图,另一个代表我们进入输入模式的视图。我们先来创建两个布局,它们都是基于Dirty Phrasebook布局,当然我们会做一些小修改以便大家能够更容易理解。

首先是默认布局。

res/layout/activity_part2.xml

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:clipChildren="false">


<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">


<View
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary" />


<View
android:id="@+id/focus_holder"
android:layout_width="0dp"
android:layout_height="0dp"
android:focusableInTouchMode="true">


<requestFocus />
</View>

<android.support.v7.widget.CardView
android:id="@+id/input_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_below="@id/toolbar">


<EditText
android:id="@+id/input"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textMultiLine" />


<ImageView
android:id="@+id/input_done"
android:layout_width="32dip"
android:layout_height="32dip"
android:layout_alignBottom="@id/input"
android:layout_alignEnd="@id/input"
android:layout_alignRight="@id/input"
android:layout_gravity="bottom|end"
android:layout_margin="8dp"
android:background="@drawable/done_background"
android:contentDescription="@string/done"
android:padding="2dp"
android:src="@drawable/ic_arrow_forward"
android:visibility="invisible" />


</android.support.v7.widget.CardView>

</RelativeLayout>

<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">


<android.support.v7.widget.CardView
android:id="@+id/translation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">


<View
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="?attr/colorPrimary" />

</android.support.v7.widget.CardView>
</FrameLayout>

</LinearLayout>

这个布局与我们上一篇使用的布局基本一致,但是稍微有一点小修改。现在我们感兴趣只有id为toolbar, focus_holder, input, input_view, input_done和 translation的视图控件。

它看起来是这样的:

The layout for when we’re in input mode is:

下面是进入输入模式的布局 :

res/layout/activity_part2_input.xml

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:clipChildren="false">


<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">


<View
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary" />


<View
android:id="@+id/focus_holder"
android:layout_width="0dp"
android:layout_height="0dp"
android:focusableInTouchMode="true" />


<android.support.v7.widget.CardView
android:id="@+id/input_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentTop="true"
android:layout_marginBottom="?attr/actionBarSize">


<EditText
android:id="@+id/input"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textMultiLine">


<requestFocus />
</EditText>

<ImageView
android:id="@+id/input_done"
android:layout_width="32dip"
android:layout_height="32dip"
android:layout_alignBottom="@id/input"
android:layout_alignEnd="@id/input"
android:layout_alignRight="@id/input"
android:layout_gravity="bottom|end"
android:layout_margin="8dp"
android:background="@drawable/done_background"
android:contentDescription="@string/done"
android:padding="2dp"
android:src="@drawable/ic_arrow_forward"
android:visibility="visible" />

</android.support.v7.widget.CardView>

</RelativeLayout>

<FrameLayout
android:layout_width="match_parent"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">


<android.support.v7.widget.CardView
android:id="@+id/translation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:visibility="invisible">


<View
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="?attr/colorPrimary" />

</android.support.v7.widget.CardView>
</FrameLayout>

</LinearLayout>

该布局和上一布局也是基本相同,它们的区别在于 :

  1. id为input_view的视图现在依附在它的父视图的顶部,而不是在toolbar的下面,也就是说它现在覆盖住了toolbar;
  2. input_done视图现在已经变为可见状态,上一个布局中它是invisible状态;
  3. translation视图变为invisible,上一布局中它是visible状态。

效果如下 :

两个布局代表UI的两个状态,如果我们使用Transitions API那么它们就是我们所谓的场景。

我们现在要做的就是在这两个布局之间进行状态切换,也就是进入、退出输入模式。首先我们需要检测MainActivity中的焦点 :

part2/mainActivity.java

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
public class MainActivity extends AppCompatActivity {

private View input;
private TransitionController focusChangeListener;
private View.OnClickListener onClickListener;
private View focusHolder;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
focusChangeListener = Part2TransitionController.newInstance(this);
onClickListener = new View.OnClickListener() {
@Override
public void onClick(@NonNull View v) {
focusHolder.requestFocus();
}
};

setContentView(R.layout.activity_part2);
}

@Override
public void setContentView(int layoutResID) {
if (input != null) {
input.setOnFocusChangeListener(null);
}
super.setContentView(layoutResID);
input = findViewById(R.id.input);
View inputDone = findViewById(R.id.input_done);
focusHolder = findViewById(R.id.focus_holder);
input.setOnFocusChangeListener(focusChangeListener);
inputDone.setOnClickListener(onClickListener);
}
}

因为我们检测到焦点变化,因此我们还需要添加一些逻辑到setContentView函数中。这样一来我们在调用setContentView时就可以切换这两个布局,此时View的层级关系也会随着改变。因此我们每次都需要找到布局中的子视图,焦点listener我们也需要移除并且重新设置一个到input视图中。

和原来一样,我们需要一个TransitionController来处理焦点变化:

.part2.Part2TransitionController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Part2TransitionController extends TransitionController {

Part2TransitionController(WeakReference<Activity> activityWeakReference, AnimatorBuilder animatorBuilder) {
super(activityWeakReference, animatorBuilder);
}

public static TransitionController newInstance(Activity activity) {
WeakReference<Activity> activityWeakReference = new WeakReference<>(activity);
AnimatorBuilder animatorBuilder = AnimatorBuilder.newInstance(activity);
return new Part2TransitionController(activityWeakReference, animatorBuilder);
}

@Override
protected void enterInputMode(Activity activity) {
activity.setContentView(R.layout.activity_part2_input);
}

@Override
protected void exitInputMode(Activity activity) {
activity.setContentView(R.layout.activity_part2);
}
}

现在我们需要做的只是调用Activity的setContentView函数来切换两个布局。

如果我们现在运行上面的代码,我们可以看到两个布局毫无过度的切换,这必然不是我们想要的。在下一篇文章中,我们将讲解如何在布局切换时添加动画。

本文的源代码在这里

CC BY-NC-SA 4.0


Manual Layout Transitions – Part 1 by Styling Android is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
Permissions beyond the scope of this license may be available at http://blog.stylingandroid.com/license-information.