0%

四川料理は 辛いです

一类形容词四种形态

四川料理は 辛いです。

四川料理は 甘く ないです。

四川料理は 苦かったです。

四川料理は 苦くなかったです。

温泉

京都の紅葉は 有名です

二类形容词+名词四种形态

京都は にぎやかです。

京都は 静かでは ありません

京都は にぎやかでした

京都は 静かでは ありませんでした

奈良は 静か 町です。 - きれいな 人形を 買いました。

この料理は どう ですか? ‐ この料理は いかがですか?

横浜は とんな町 ですか?

美術館

休みの日は 観光客が 多いです。

小野さんは 歌が 好きです

形容词作动词用,一类是情感形容词,一类是能力形容词。

小野さんは 犬 怖いです。・こわい

王さんは 肉 嫌いです。 ・きらい 

小野さんは 日本語が 上手です。・じょうず

小野さんは 日本語が 下手です。・へた

小野さんは 日本語が 苦手です。・にがて

小野さんは 日本語が できます。・わかりません

お土産

この近くのホテルで 友達の写真展が ありますから。

李さんは 森さんより 若いです

森さんより 李さんの ほんが 若いです。

李さんは 森さんほど 若く ないです。 静かでは ありません。

 の 中で 李さんが いちばん 若いです。

の 中で 誰が いちばん 若いです?

​ 李さんが いちばん 若いです。

李さん 森さんと どちらが 若いですか?

​ 李さんの ほんが 若いです。

森さんは テニスが 上手ですね?

​ いいえ、長島さんほど 上手では ありません

お茶とお酒・さけ

单词

天气

  • 天気予報・よほう 
  • 晴・はれ 曇・くもり 
  • 雨・あめ 雪・ゆき 霧・きり 雷・かみなり 風・かぜ 
  • 虹・にじ 
  • 蒸し暑い・む 
  • 暖かい・あたた 涼しい・すず 
  • 冬・ふゆ 春・はる 秋・あき

地名

札幌・さっぽろ 名古屋・なごや  仙台・せんだい 新潟・にいがた 

神戸・こうべ  高知・こうち   長崎・ながさき 福岡・ふくおか

渋谷・しぶや  新宿・しんじゅく 秋葉原・あきはばら 上野・うえの

広州・こうしゅう 昆明・こんめい 桂林・けいりん 紹興・しょうこう

大連・だいれん 天津・てんしん  北京・ぺきん  西安・せいあん

武漢・ぶかん  杭州・こうしゅう 成都・せいと  重慶・じゅうけい

片假名

  • スープ 
  • ニュース 
  • グラス
  • シーズン 
  • ハンサム 
  • カラオケ 
  • ロック 
  • ポップス 
  • クラシック
  • スポーツ 
  • ゴルフ
  • バラ
  • ナシ 🍐 
  • バナナ 🍌 
  • ジュース 

生词

  • すき焼き 

  • 眺め・ながめ 

  • 気持ち・きもち 

  • たくさん 

  • 通り・とおり 

  • 所・ところ 

  • お菓子・おかし 

  • 平日・へいじつ 

  • 生活・せいかつ 

  • もう少し・すこ 

  • いかが・如何 

  • どうして

  • だから・ですから

  • 別荘・べっそう 

  • また・たまに・よく

  • 等々・など 

  • 季節・きせつ 

  • 緑茶・りょく

  • 大好き・だいすき 

形容词

  • 新しい・あたら 古い・ふるい 
  • 高い 低い・ひく
  • 難しい・むずか 易しい・やさ 
  • いい 悪い・わる 
  • 熱い 冷たい・つめたい 
  • 多い 少ない・すく 
  • 広い 狭い・せま 
  • 遠い・とお 近い・ちか 
  • 楽しい・たの つまらない おもしろい
  • 美味しい 不味い・まずい
  • 辛い・からい 苦い・にがい 甘い・あまい 塩辛い・しお 酸っぱい・すっ
  • 好き・すき 嫌い・きらい 
  • 便利・べん 不便・ふべん 
  • 元気・げんき
  • にぎやか 静か 
  • きれい 汚い・きたない
  • 親切・しんせつ 暇・ひま 

森さんは 七時に おきます

动词+时间 に・から・まで

毎日、何時寝ますか。

昨日、何時から 何時まで 働きますか。

試験は いつ 始まりますか。

試験は 何時 はじまりますか。

遅刻・こく

吉田さんは 来月中国へ 行きます 

李さんは 友達と 来月 北京から 新幹線で 来ました。

いつ アメリカへ 行きますか。

​ ‐ 十月に 行きます。

交通機関 つう・通

  • 何で 帰りましたか?
  • 電車です。 渋谷まで 電車で 行きました。駅から アパートまで 歩いて 帰りました。
  • 小野さん?
  • 私は 電車です。駅からは タクシーで 家へ帰りました。

李さんは 毎日 コーヒーを 飲みます

李さん、今朝 うちで 新聞を 読みましたか? 

  • いいえ、読みませんでした。

今朝、何を 食べましたか?

  • 何も 食べませんでした。

昼ご飯

  • いつも コンビニですか。

    • いいえ。いつもは 蕎麦屋で そばかうどんを 食べます。
  • じゃあ、課長、失礼します。-しつれい 

    • いってらっしゃい
  • いらっしゃいませ

    • かしこまりました。

李さんは 日本語で 手紙を 書きます

动词+补语和第三方

私は 小野さんに お土産を あげます。

私は 小野さんに 辞書を もらいました。

その映画を 誰に あげますか?

その映画を 誰に もらいますか?

何で 送りましたか? 送り・おく

  • 航空便で 送りました。 航空便・こうくうびん

スケジュール表・ひょう

さっき、長島さんに 電話を もらいました。

ファックスを 届きましたか?

单词

時間

あととい 昨日 今日 明日 明後日・あさって 毎日
今朝 毎朝
昨夜・ゆうべ 今晩・こんばん 毎晩
先々週・せん 今週・こん 再来週・さらい 毎週
先々月 今月・こんげつ 再来月 毎月・つき
あととし 近年・きん 今年・ことし 再来年・ねん 毎年・とし
1日・ついたち 2日・ふつか 3日・みっか 4日・よっか 5日・いつか 6日・むいか
7日・なのか 8日・ようか 9日・ここのか 10日・とおか じゅういちにち
14・じゅうよっか
20日・はつか 24日・にじゅうよっか
一月・がつ 二月 三月 四月
五月 六月 七月・しち 八月
九月 十月 十一月 十二月

片假名

  • パーティー 
  • コンサート
  • クリスマス 
  • バス 
  • フェリー 
  • アパート 
  • プール 
  • 北京・ぺきん 
  • コーヒー・コーラ
  • パン・ケーキ
  • カリー 
  • チーズ 
  • リンゴ 
  • イチゴ 
  • デニス
  • ジョギング
  • サッカー
  • パンダ
  • プレゼント 
  • チケット 
  • パンフレット
  • ファックス
  • バールペン
  • チョコレート 
  • アイスクリーム 
  • スプーン

生词

  • 学校・がっこう 
  • 仕事・しごと 
  • 出張 しゅっちょう 
  • 子供の日 ひ
  • まっすぐ 
  • 北海道・ほっかいどう 
  • 渋谷・しぶや 
  • 箱根・はこね 
  • 新宿・しんじゅく
  • 申込書・もうしこみ 
  • お金・がね 
  • 住所・じゅうしょ 
  • 小麦粉・こむぎこ 
  • 速達・そくたつ
  • お疲れ様でした・つか・さま
  • 大変ですね 

动词

  • 送り・おく 出し・だ メールを 出します・だし
  • 届き・とど
  • 作り・つく 資料を 作ります
  • 太り・ふと 
  • 貸します・か 
  • 習い・ならい  
  • あげ 窓を 開けます・あけ ー 閉めます・しめ  
  • 教え・おし
  • 始まり・はじまり - 終わり・おわり 
  • 顔を 洗います・かお あらい 
  • 服を 着ます・き  
  • 煙草を 吸います・すい 
  • 切手を 貼ります・はり 
  • 降り
  • 歯を 磨きます・は みがき 
  • お茶を 入れます・いれ 
  • 靴を はきます - 脱ぎます・ぬぎ 
  • お金を 払います・はらい 
  • 電気を つけます ー 消します・け

李さんは 中国人です

出迎え


        

これは 本です

疑问指示代词

  • 誰(だれ)ー そのノートは だれの ですか。
  • 何(なに)ー それは 何ですか。
  • どれ(which)ー 森さんのかばんは どれ ですか。
  • どの(which)ー 小野さんの机は どの机 ですか。
  • どなた(人)- この方は どなた ですか。  

家族の写真


        

ここは デパートです

疑问指示代词

  • どちら - お国は どちらですか。
  • どこ  - 会社は どこですか。

ホテルの周辺


        

部屋に 机といすが あります

方位代词

  • 上 うえ - 下 した
  • 前 まえ - 後 うしろ 
  • 中 なか - 外 そと 
  • 隣 となり ‐ 近 ちかく

机の上に 猫が あります。

売店は 駅の外に あります。

その箱の中に 何が ありますか。

部屋に 誰が いますか。

小野さんの家は どこに ありますか。

会社の場所


        
  • 小野さん、会社は どこに ありますか。

    • ここです。
  • 近くに 駅が ありますか。

    • Jℝと地下鉄の駅が あります。Jℝの駅は ここです。
  • 地下鉄の 駅が ここですか。

    • ええ、そうです。Jℝの駅の隣に 地下鉄の駅が あります。

单词


        

        

        

        

地理

  • アメリカ・AME RIKA
  • フランス・FU RAN SU
  • イギリス・IGI RISU 英国
  • イタリア・ITA RIA 大利 
  • インド・IN DO  
  • オーストラリア・OSUTO RARIA
  • スパイン・SU PA IN
  • ドイツ・DO I TSU 
  • ロシア・ROSIA
  • アジア・AJIA ヨーロッパ・YUROPA アフリカ
  • 北アメリカ大陸・きた・だいりく
  • 南アメリカ大陸・みなみ 

建筑物

  • 館(び じゅつ) 
  • 体育館(たい いく) 
  • 物館(はく ぶつ) 

  • 園(どう ぶつ) 

  • 遊園地(ゆう えん ち) 
  • 所(し やく) 
  • 署(けい さつ しょ) 
  • 消防(しょう ぼう しょ)
  • 薬局(やっ きょく) 
  • 工場(こう じょう) 
  • 場(げき) 
  • 駐車場(ちゅう)
  • 床屋(とこ)
  • 肉屋(にく)
  • 魚屋(さかな)
  • 八百屋(や お や) 
  • お店(みせ)
  • 空港(くう こう) 
  • 学校(がっ こう)

数字

  • 零 れい - 令和 れいわ 
  • 四 よん・し 
  • 七 しち・なな 
  • 九 く・きゅう 
  • 0.3 れいてんさん 
  • 1/3 さんぶんいち 
  • 100 ひゃく
  • 300 さんびゃく 
  • 600 ろっぴゃく 
  • 700 ななひゃく 
  • 800 はっぴゃく 

ご両親

  • そふ お爺さん・じい そば おばあさん
  • ちち お父さん はは お母さん 
  • 息子・むすこ 娘・むすめ お嬢さん・じょう
  • ご兄弟・きょうだい 
  • あに お兄さん・にい あね お姉さん・ねえ
  • おじさん・叔父 おばさん・叔母

字母

エー ビー シー ディー イー エフ ジー
エイチ アイ ジェー ケー エル エム
オー ピー キュー アール エス ティー
ユー ブイ ダブリュー エックス ウイ ゼット

片假名

  • カメラ  デジカメ写真 ビデオ
  • テレビ 
  • パソコン 
  • ラジオ 
  • シルク 
  • ハンカチ 
  • デパート
  • マンション・ホテル 
  • コンビニ
  • レストラン 
  • トイレ
  • バーゲン会場 じょう
  • エスカレーター 
  • コート
  • スイッチ 
  • ベッド 
  • サッカーボール
  • ビール 
  • ウイスキー 

生词

  • お土産・おみやげ 
  • 本棚・だな 
  • 壁・かべ 
  • 眼鏡・めがね 
  • 子供・ども 
  • 妹・いもうと 
  • 女・おんな 男・おとこ

android 中使用字体是非常不方便的,因为不支持 xml 定义,必须使用代码来动态修改字体,也无法用样式或主题来批量修改字体。

现在我们来探索下面两个字体使用增强库的原理。

android-typeface-helper

android-typeface-helper 是较为简单,使用方法如下

1.首先在 Application 中初始化,主要是创建和缓存字体

1
2
3
4
5
6
7
8
@Override public void onCreate() {
super.onCreate();
// Initialize typeface helper
TypefaceCollection typeface = new TypefaceCollection.Builder()
.set(Typeface.NORMAL, Typeface.createFromAsset(getAssets(), "fonts/ubuntu/Ubuntu-R.ttf"))
.create();
TypefaceHelper.init(typeface);
}

2.对 Activity , ViewGroup 或 View 应用字体。

1
2
3
4
5
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_layout);
TypefaceHelper.typeface(this);
}

也可以动态的改变字体。

1
public static void typeface(View view);

这个库是实际没有做太多事,仅仅对常规字体使用方法的包装而已。typeface 方法如下

1
2
3
4
5
6
7
8
9
private static void applyForView(View view, TypefaceCollection typefaceCollection) {
if (view instanceof TextView) {
TextView textView = (TextView) view;
Typeface oldTypeface = textView.getTypeface();
final int style = oldTypeface == null ? Typeface.NORMAL : oldTypeface.getStyle();
textView.setTypeface(typefaceCollection.getTypeface(style));
textView.setPaintFlags(textView.getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG);
}
}

可见字体的更改只应用在 TextView 控件上,对于 ViewGroup 要执行递归更改,对于 Acitivity 实际是对 content 布局执行递归更改。

值得一提的是你还可以创造某种字体的 SpannableString

1
public static SpannableString typeface(CharSequence sequence, TypefaceCollection typefaceCollection);

这是通过定义字体 span 获得的效果,不过首先通过集合提供字体对象

1
2
3
4
5
6
7
8
private static class TypefaceSpan extends MetricAffectingSpan {
Typeface typeface;

@Override public void updateDrawState(TextPaint tp) {
tp.setTypeface(typeface);
tp.setFlags(tp.getFlags() | Paint.SUBPIXEL_TEXT_FLAG);
}
}

Calligraphy

使用 Calligraphy 这个库,你可以直接在 XML 文件中定义字体

1
2
3
4
<TextView fontPath="fonts/MyFont.ttf"/>
<style name="TextAppearance.FontPath" parent="android:TextAppearance">
<item name="fontPath">fonts/RobotoCondensed-Regular.ttf</item>
</style>

自定义控件时添加一个属性并不稀奇,稀奇的是给系统控件 TextView 添加了一个属性 fontPath,而且生效了。

要做到这点,首先要在 Application 中进行设置。

1
2
3
4
5
6
7
8
@Override
public void onCreate() {
super.onCreate();
CalligraphyConfig.initDefault(new CalligraphyConfig.Builder()
.setDefaultFontPath("fonts/Roboto-RobotoRegular.ttf")
.setFontAttrId(R.attr.fontPath)
.build()
);

此外还需要在 Activity 中替换 Context

1
2
3
4
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase));
}

当然该库也提供了 CalligraphyTypefaceSpan 供使用,不再叙述。

LayoutInflater的原理解析

android 是怎样解析属性的呢?核心是更改 LayoutInflater,该类的加载过程如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public View inflate(@LayoutRes int resource, ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
final XmlResourceParser parser = res.getLayout(resource);
AttributeSet attrs = Xml.asAttributeSet(parser);

View result = root;
View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
temp.setLayoutParams(params);
}

if (root != null && attachToRoot) {
root.addView(temp, params);
}
if (root == null || !attachToRoot) {
result = temp;
}
return result;
}

这个过程实际是从布局文件中解析出 View 对象,再讲其添加到容器上去的过程。过程中有以下几点值得注意

1.attachToRoot 参数决定了是要要将View添加到容器上,以及返回值是容器自身还是View对象。

2.根据XML解析器生成View对象即 createViewFromTag 方法是一个移花接玉的过程。

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
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs){
TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
int themeResId = ta.getResourceId(0, 0);
context = new ContextThemeWrapper(context, themeResId);

View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}

if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs); //使用反射创建
} else {
view = createView(name, null, attrs); //使用反射创建
}
}
return view;
}

这里创建View的处理顺序是 Factory2,Factory1,PrivateFactory,最后才是使用反射来创建。

我们知道通过 Factory2 可以改变View的创建过程,实际上 Activity 就是这样做的,在执行 super.onCreate(savedInstanceState);这一句时就设置了 PrivateFactory。

简单实现

我们通过自定义一个 LayoutInflater 来实现简单的效果,首先定义一个属性

1
<attr name="customFont" format="string"/>

在xml中使用

1
<TextView customFont="fonts/Rotobo-Bold.ttf"/>

这时该属性已经能够被 LayoutInflater 解析到 AttributeSet 参数中去。我们可以使用 Factory2 接口来观察这一现象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Factory2 只能设置一次,所以先用反射修改 mFactorySet 的值,以解除此限制
LayoutInflater inflater = LayoutInflater.from(this);
Class cls = LayoutInflater.class;
Field bool = cls.getDeclaredField("mFactorySet");
bool.setAccessible(true);
bool.setBoolean(inflater, false);
inflater.setFactory2(new LayoutInflater.Factory2() {

@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if(name.equals("TextView")) {
int count = attrs.getAttributeCount();
for (int i = 0; i < count; i++) {
Log.i(TAG, attrs.getAttributeName(i)); //customFont
Log.i(TAG, attrs.getAttributeValue(i));//fonts/Rotobo-Bold.ttf
}
return null;
}
});

从这里就可以拿到资源值(即字体文件路径)了,利用这一属性就可以设置字体了,简易实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if(name.equals("TextView")) {
TextView textview = new TextView(MainActivity.this);
textview.setLayoutParams(new ViewGroup.LayoutParams(400, 100));
textview.setText("中华大道 English hello 123456");
int count = attrs.getAttributeCount();
for (int i = 0; i < count; i++) {
if(attrs.getAttributeName(i).equals("customfont")){
String path = attrs.getAttributeValue(i);
Typeface typeface = Typeface.createFromAsset(getAssets(), path);
textview.setTypeface(typeface);
}
}
return textview;
}
return null;
}

这个简单的实现已经能通过自定义属性来修改字体文件了,但离具体应用还差的比较远,比如它无法使用 style 和主题,也会屏蔽掉其它View的生成,此外还默认设置成默认应用在 Activity 上。

在Activity中应用字体属性

在Activity中应用字体属性就是要修改其中的LayoutInflater类,通过调查我们可以发现该类已经实现了 Factory2 接口,只要通过代理的方式来改造该接口的实现使其能够处理字体属性就行了。

实际过程分为两步

1.替换 Factory2 接口

1
2
3
4
5
6
7
8
9
10
public LayoutInflater getLayoutInflater() {
LayoutInflater inflater = super.getLayoutInflater();
final LayoutInflater.Factory2 factory2 = inflater.getFactory2();
Class cls = LayoutInflater.class;
Field bool = cls.getDeclaredField("mFactorySet");
bool.setAccessible(true);
bool.setBoolean(inflater, false);
inflater.setFactory2(new WrapFacory2(factory2));
return inflater;
}

注意一定要做如下设置,在活动创建时就替换 LayoutInflater 来

1
2
3
4
5
6
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getLayoutInflater(); //改造已经存在的 LayoutInflater 对象
setContentView(R.layout.activity_main);
}

LayoutInflater 对象在 Window 中,必须要调用 getLayoutInflater 来改造它,才能在活动中生效。

2.利用代理扩展Factory2的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private class WrapFactory2 implements LayoutInflater.Factory2 {
LayoutInflater.Factory2 factory2;
public WrapFactory2(LayoutInflater.Factory2 factory2) {
this.factory2 = factory2;
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = factory2.onCreateView(parent, name, context, attrs);
if(view instanceof TextView){ //支持所有 TextView 的子类,如Button
int count = attrs.getAttributeCount();
for (int i = 0; i < count; i++) {
if(attrs.getAttributeName(i).equals("customfont")){
String path = attrs.getAttributeValue(i);
Typeface typeface = Typeface.createFromAsset(getAssets(), path);
((TextView)view).setTypeface(typeface);
}
}
}
return view;
}
}

这样字体属性在这个 Activity 中就可以生效了,而且完全不干扰其它控件和其它属性。

在样式或主题中使用字体属性

首先定义一个样式

1
2
3
<style name="Text" >
<item name="customFont">fonts/gtw.ttf</item>
</style>

此时应用样式的主要问题就是如何使用 AttributeSet 解析出其中的字体属性,方法如下

1
2
3
4
5
6
7
8
if(attrs.getAttributeName(i).equals("style")){
int styleId = attrs.getStyleAttribute();
TypedArray ta = obtainStyledAttributes(styleId, new int[]{R.attr.customFont});
String path = ta.getString(1);
ta.recycle();
Typeface typeface = Typeface.createFromAsset(getAssets(), path);
((TextView)view).setTypeface(typeface);
}

如果在主题中定义了字体属性,因为 AttributeSet 中不会有未使用的属性出现,自然就解析不出字体属性了。

1
2
3
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="customfont">fonts/gtw.ttf</item>
</style>

这时应该用主题对象(Theme)来解析出字体属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = factory2.onCreateView(parent, name, context, attrs);
if(view instanceof TextView && attrs!=null){
TypedArray ta = getTheme().obtainStyledAttributes(new int[]{
android.R.attr.colorAccent, //这里必须要以系统属性开头,否则自定义属性将解析错误
R.attr.customFont, //原因有待探索
});
String path = ta.getString(1);
ta.recycle();
Typeface typeface = Typeface.createFromAsset(getAssets(), path);
((TextView)view).setTypeface(typeface);
}
return view;
}

最后在实现时应该处理获取属性的优先级,一般而言,直接定义的优先级最高,style其次,主题再次之。

PopupWindow 的根本构造是

1
2
//内容控件不能有 parent
public PopupWindow(View contentView, int width, int height, boolean focusable)

本质上是利用 WindowManager 将 View 添加到屏幕上去,这也是它为何有包括 内容布局,尺寸,是否聚焦,Transition动画,BackgroundDrawable,Elevation,AnimationStyle等在内众多参数的原因,这些参数为 WindowManager.LayoutParams 所必须。

值得一提的是动画类型 AnimationStyle 是 style定义的,如果要改变动画类型,亦需要提供 style 资源。

1
2
3
4
<style name="popupwindow_anim_style">
<item name="android:windowEnterAnimation">@android:anim/fade_in</item>
<item name="android:windowExitAnimation">@android:anim/fade_out</item>
</style>

PopupWindow 在屏幕上显示的方法如下

1
2
//此处 View 主要是提供窗口服务的 IBinder 对象,因此随便一个控件即可以。
void showAtLocation(View parent, int gravity, int x, int y)

应该注意的是内容控件被套了两层 FrameLayout 的包装,第一层设置 BackgrondDrawable,第二场才被设置 WindowManager.LayoutParams 参数并添加到屏幕上去。

也可以使用下列方法来显示,不过期参数已经指向左上角

1
public void showAsDropDown(View anchor)

动画效果

设置 AnimationStyle 可以达到动画效果

1
2
3
4
<style name="PopupW">
<item name="android:windowEnterAnimation">@anim/slide_in_left</item>
<item name="android:windowExitAnimation">@anim/slide_out_right</item>
</style>

在 23 以上,还可以采用Transition 达到动画效果

1
2
p.setEnterTransition(new TransitionSet().addTransition(new Slide(Gravity.BOTTOM)));
p.setExitTransition(new TransitionSet().addTransition(new Slide(Gravity.RIGHT)));

菜单

菜单包括如活动中的Option和Context菜单,Toolbar上的菜单,Navi上的菜单以及 PopupMenu 等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<item android:title="更多">    

<menu>
<item android:title="设置"/>
<item android:title="关于"/>
</menu>

<item
android:id="@+id/action_search"
android:icon="@drawable/ic_action_search"
android:title="@string/action_search"
app:showAsAction="collapseActionView|always|withText"
app:actionViewClass="android.support.v7.widget.SearchView" />
</item>

MenuInflater 类负责将XML文件实例化为 Menu 对象,其对于 Menu ,Group,SubMenu 等标签有不同的解析方法。

菜单中的 ActionView,可以改变菜单外观,如搜索框中所含的 SearchView 控件

1
2
3
4
MenuItem searchItem = menu.findItem(R.id.action_search);
SearchView searchView = (SearchView) searchItem.getActionView();
MenuItemCompat.setOnActionExpandListener(searchItem, ...);
MenuItemCompat.expandActionView(searchItem);

你也可以主动为菜单设置 ActionView。

PopupMenu

PopupMenu 内部有一个 Menu 对象,将xml资源加载到该对象上,显示在锚定控件下方,可以指定对齐方式。

1
2
3
4
PopupMenu popupMenu = new PopupMenu(this, textView);
popupMenu.getMenuInflater().inflate(R.menu.main, popupMenu.getMenu());
textView.setOnTouchListener(popupMenu.getDragToOpenListener());
popupMenu.show();

关于 OverflowButton

1.是否永久显示 overflow菜单

使用反射修改 ViewConfiguration 的 sHasPermanentMenuKey 参数既可以永久显示。

2.获取并修改 Overflow 按钮

a.可以使用主题修改

1
2
3
4
5
6
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:actionOverflowButtonStyle">@style/OverflowStyle</item>
</style>
<style name="OverflowStyle">
<item name="android:src">@android:drawable/arrow_up_float</item>
</style>

b.可以使用代码修改

1
2
3
4
5
ArrayList<View> outViews = new ArrayList<View>();
String overflowDescription = activity.getString("more options");
decorView.findViewsWithText(outViews, overflowDescription, View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION);
TintImageView overflow=(TintImageView) outViews.get(0);
overflow.setImageResource(imageID);

Dialog

实际上 Activity 只要使用 Dialog 主题就可以看做Dialog,但 Dialog 实际上是没有生命周期的。

主要的话框是 AlertDialog,以及较新的 DialogFragment 和 BottomDialog ,也包括一些特定功能的对话框如 ProgressDialog,颜色,日期等。

AlertDialog 只不过预设了大量布局且可以根据构造方法来选择控件而已,这里提一下单选和多选对话框的使用。创建时应使用适配器,这样可以在对话框上添加条目

1
Builder setAdapter(final ListAdapter adapter, final OnClickListener listener)

ProgressDialog 的实现原理就是内置了一个 ProgressDialog 。

BottomSheetDialog

使用方法如下

1
2
3
BottomSheetDialog dialog=new BottomSheetDialog(this);
dialog.setContentView(dialogView);
dialog.show();

这里设置布局时加入布局时做了一番手脚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private View wrapInBottomSheet(int layoutResId, View view, ViewGroup.LayoutParams params) {
CoordinatorLayout coordinator = inflate(context,
R.layout.design_bottom_sheet_dialog, null);
FrameLayout bottomSheet = (FrameLayout) coordinator.findViewById(R.id.design_bottom_sheet);
mBehavior = BottomSheetBehavior.from(bottomSheet);
mBehavior.setBottomSheetCallback(mBottomSheetCallback);
bottomSheet.addView(view, params);
coordinator.findViewById(R.id.touch_outside).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mCancelable && isShowing() && shouldWindowCloseOnTouchOutside()) {
cancel();
}
}
});
return coordinator;
}

这里设置布局后,将载入一个 CoordinatorLayout,其中有一个 FrameLayout,配置 BottomSheetBehavior,并将内容添加进去。

加载的主题是 Window.FEATURE_NO_TITLE,即没有标题的对话框。

1
private BottomSheetBehavior<FrameLayout> mBehavior;

DialogFragment

使用 DialogFragment 是因为 Dialog 无生命周期,在横竖屏切换时会导致丢失对话框,而 DialogFragment 本质是一个带 Dialog 对象的碎片,能够恢复和重建 。

实际上它不需要重写 onCreateView 方法来建立视图,只需要重写下列方法建立 Dialog 对象即可,真正显示的是 Dialog 对象而不是视图。

1
Dialog onCreateDialog(Bundle savedInstanceState)

而后我们使用下列方法显示对话框

1
2
void show(FragmentManager manager, String tag)
int show(FragmentTransaction transaction, String tag)

这实际是添加到任务栈,用第二个方法你可以的碎片在栈中的 id。这样碎片就会追随所依附活动的状态,一旦碎片处于 onStart 状态 Dialog 就会显示,而 碎片处于 onStop 状态时 Dialog 就会隐藏(不是dismiss)。

如果突然关闭所在页面,活动销毁会导致碎片销毁视图,经历 DestroyView 状态,则 Dialog 消失(即dismiss),接着会触发碎片的销毁,一般而言是将碎片出栈,以释放内存。

此外下列方法也将完成碎片的释放

1
public void dismiss();

问题1:显示的是 Dialog 还是 View?

下列方法被改写了,这在创建视图时也影响了 Dialog 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public LayoutInflater getLayoutInflater(Bundle savedInstanceState) {

if (!mShowsDialog) {
return super.getLayoutInflater(savedInstanceState);
}
mDialog = onCreateDialog(savedInstanceState);
if (mDialog != null) {
setupDialog(mDialog, mStyle);
return (LayoutInflater) mDialog.getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
}
return (LayoutInflater) mHost.getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
}

首先 mShowsDialog 标记了是否显示对话框,如果其为 false,那么正常加载视图进行显示。

而后创建 Dialog 对象,在 onActivityCreated 方法中将决定显示哪个?如果同时设置了 View,则将 View 作为 Dialog 的内容视图,此外还要给 Dialog 打上 dismiss 监听器,保证碎片的释放。

因此如果想采用所创建的对话框,第一要保证 mShowsDialog 标记为真,第二不要画蛇添足去实现 onCreateView 方法。

更改 Dialog 主题

以下是V7包里 AlertDialog 的实际布局,实际是由 标题块(带分割线),message块,自定义块和按钮块 组成的。

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
<LinearLayout android:id="@+id/parentPanel">

<LinearLayout android:id="@+id/topPanel">
<LinearLayout android:id="@+id/title_template">
<ImageView android:id="@+id/icon"
android:src="@drawable/ic_dialog_info" />
<com.android.internal.widget.DialogTitle android:id="@+id/alertTitle"
style="?android:attr/textAppearanceLarge"
android:singleLine="true" />
</LinearLayout>
<ImageView android:id="@+id/titleDivider"
android:src="@android:drawable/divider_horizontal_dark" />
</LinearLayout>

<LinearLayout android:id="@+id/contentPanel>
<ScrollView android:id="@+id/scrollView">
<TextView android:id="@+id/message"
style="?android:attr/textAppearanceMedium" />
</ScrollView>
</LinearLayout>

<FrameLayout android:id="@+id/customPanel">
<FrameLayout android:id="@+android:id/custom" />
</FrameLayout>

<LinearLayout android:id="@+id/buttonPanel" >
<LinearLayout style="?android:attr/buttonBarStyle">
<LinearLayout android:id="@+id/leftSpacer"
android:layout_weight="0.25"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone" />
<Button android:id="@+id/button1"
android:layout_width="0dip"
android:layout_weight="1"
style="?android:attr/buttonBarButtonStyle" />
<Button android:id="@+id/button3"
android:layout_width="0dip"
android:layout_gravity="center_horizontal"
android:layout_weight="1"
style="?android:attr/buttonBarButtonStyle" />
<Button android:id="@+id/button2"
android:layout_width="0dip"
android:layout_weight="1"
style="?android:attr/buttonBarButtonStyle" />
<LinearLayout android:id="@+id/rightSpacer"
android:layout_width="0dip"
android:layout_weight="0.25"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

想要修改主题可以用如下构造方法

1
new AlertDialog.Builder(this, R.style.Test);

这里可以传入主题,也可以传入样式,其中主题 id 均大于 0x01000000,如果是样式,则将样式应用到主题上。

1
context.getTheme().resolveAttribute(R.attr.alertDialogTheme, outValue, true);

Dialog 的扩展:布局定制和动画效果

Dialog 的扩展主要包括两个方面:布局定制和动画效果。

想实现 Dialog 的入场动画效果,最简单的方法如下

1
2
3
4
5
6
7
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface d) {
View view = dialog.getWindow().getDecorView();
view.animate().rotation(360).setDuration(2000).start();
}
});

这种方法即在 DecorView 做动画,但局限性很大。

更一般的方式是定制一个布局,或像 BottomSheetDialog 那样,对传入的布局采用 wrap 操作;最终留出根布局的接口,以便对此进行动画。

布局定制的核心是改造 onCreate 方法

1
2
3
4
5
6
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.app_bar_main);
// ......
}

动画效果的核心是设置具体的动画效果,对于显示动画,可以放在 OnShowListener 接口中,因为该接口执行的时候,布局已经添加到 Dialog 中去了,但对于消失动画而已,则无法利用 onDiamissListener 接口,因此此接口执行时,布局已经被移除。

此时,可以通过改造 dismiss 方法来实现消失动画。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void dismiss() {
animatorSet = new AnimatorSet();
setupAnim();
animatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationEnd(Animator animation) {
AnimDialog.super.dismiss();
}
});
animatorSet.resume();
}

这个方法一定要具体的子类( 此处是AnimDialog)来执行,不能交给父类AlertDialog执行,因此创建一个构造方法

1
2
3
public static AnimDialog create(Context context) {
return new AnimDialog(context);
}

动画的具体效果

rotationY 是绕着y轴旋转的,为了动画之后显示正常,结束值应该设置为0。起始值如果为正,就是侧对着的感觉,有“掀开”的感觉。

1
ObjectAnimator.ofFloat(view, "rotationY", -90, 0).setDuration(mDuration)

rotation 是绕着z轴旋转的,所有旋转操作的中心点都在中心处。

translationX等是平移的效果,改变方向可以取得从四边滑入的效果,也可以取得震动效果

1
ObjectAnimator.ofFloat(view, "translationX", 0, .10f, -25, .26f, 25,.42f, -25, .58f, 25,.74f,-25,.90f,1,0)

scaleX等可以取得展开和收缩的效果。

复合效果1:左侧旋转滚入,一般而言此类效果的方向是垂直的,从左侧滚入,那么旋转就是绕Y轴的

1
ObjectAnimator.ofFloat(view, "rotationY", 90, 0).setDuration(2000), //负责旋转ObjectAnimator.ofFloat(view, "translationX", -300, 0).setDuration(2000), //负责左侧ObjectAnimator.ofFloat(view, "alpha", 0, 1).setDuration(2000*3/2)

复合效果2:弹出展开与收缩展开

1
2
3
4
ObjectAnimator.ofFloat(view, "rotationY", 90,88,88,45,0).setDuration(2000),
ObjectAnimator.ofFloat(view, "alpha", 0,0.4f,0.8f, 1).setDuration(2000*3/2),
ObjectAnimator.ofFloat(view, "scaleX", 0,0.5f, 0.9f, 0.9f, 1).setDuration(2000),
ObjectAnimator.ofFloat(view,"scaleY",0,0.5f, 0.9f, 0.9f, 1).setDuration(2000)

弹出展开的特殊例子是新闻风格,核心是快速旋转多个来回。

1
2
3
4
ObjectAnimator.ofFloat(view, "rotation", 1080,720,360,0).setDuration(2000),
ObjectAnimator.ofFloat(view, "alpha", 0, 1).setDuration(2000*3/2),
ObjectAnimator.ofFloat(view, "scaleX", 0.1f, 0.5f, 1).setDuration(2000),
ObjectAnimator.ofFloat(view,"scaleY",0.1f,0.5f,1).setDuration(2000)

广告对话框

广告对话框

自定义View的套路包括以下四种。

绘制效果型

直接使用 Canvas 添加绘制效果是最简单的一种套路。例如BaiduWave的效果不过是通过截断 Canvas 来实现的。

又比如SubmitButton的动画效果看起来很华丽,但可以将其过程拆成几段分别进行绘制。

PatternLockView

PatternLockView

PatternLockView 的绘制效果还与触摸事件有关,属于此类型中较为复杂的。

偷梁换柱型

偷梁换柱型指的是在原生的系统控件上添加或替换控件,典型的例子就是 BadgeView

BadgeView 是怎样实现给已经存在的控件“打上”标记呢?

将原来的控件(如 ImageView)从视图树中移除,但记录它的布局参数,ID等信息。而后新建一个 FrameLayout ,将ImageView 和准备好的角标加入这个布局,最后将 FrameLayout 添加到视图树中去,并复原它的 id ,布局参数等信息。

实际上要实现 Badger 效果,完全可以靠扩展控件的 onDraw 方法来实现,不过这种实现没有通用性。

升级版的BadgeView还实现了动画效果。

AndroidResideMenu

AndroidResideMenu 在Activity的根布局中插入新布局,点击后用动画。还要处理 Activity 的 dispatchTouchEvent方法。

ShowcaseView

TapTargetView

TapTargetView 显示 SpannableString作为提示。

GuillotineMenu-Android

这个库的交互方式非常有意思,它将换上的布局旋转-90度隐藏起来,而后执行动画显示。难点在于如何处理遮挡NaviMenu的问题。

组合排列型

组合排列型是将几种控件组合在容器中,也是较为简单和常用的方法。

SpaceTabLayout

这里是将组合和绘制结合在一起了。

AndroidTreeView

这个树结构的View是纯粹的排列组合式,不过特殊之处在于要设计节点的的数据结构,即树。节点的抽象类TreeNode如下

1
2
3
4
5
public class TreeNode {
private TreeNode mParent;
private final List<TreeNode> children;
private BaseNodeViewHolder mViewHolder;//布局
}

各层的容器实际是 LinearLayout。获得根节点后,将每一个子节点中的布局添加进去即可,唯一值得注意的是这个一个递归过程,你还可以决定是否要扩展或收缩整个布局以及是否使用动画效果。

动画驱动型

动画驱动型泛指为添加动画行为,与其它几种类型可以同时使用。例如 KenBurnsView就是在ImageView上应用属性动画而已。

在比如SparkButton是对自定义的绘制效果使用动画。

FloatingView

FloatingView 是偷懒换柱的实现,兼备动画效果。

FloatingView 在 DecorView 上添加了一层 FrameLayout,确定 anchorView 在屏幕上的位置将其在添加的FrameLayout上复制,并执行动画。

1
2
3
4
5
6
7
8
Rect rect = new Rect();
anchorView.getGlobalVisibleRect(rect);
int[] location = new int[2];
mFloatingDecorView.getLocationOnScreen(location);
rect.offset(-location[0], -location[1]);
//anchorView 的测量和布局
mFloatingDecorView.addView(targetView,lp);
//此后是应用动画

类似的是HeartLayout

folding-cell-android

使用翻页动画来显示简介和详情,有意思的交互。

FoldingCell 本身是一个容器,将若干个布局折叠存放起来,点击时对这些页执行动画。

AndroidFillableLoaders

VectAlign

AndroidViewHover

Grav

综合使用

例如Context-Menu这个库同时使用了偷梁换柱和组合排列,还添加了动画效果。

有一些效果有多种实现方式,例如文字的跑马灯效果,可以纯粹绘制路径的方式实现,也可以用控件的组合的方式实现。

MarqueeLayoutLibrary将控件并排排列,每一个子控件的尺寸都与容器相同,利用 Scroller 产生滚动效果。

RotatingText

则采用直接绘制的方式完成。

DiagonalLayout

StepView

StepView 是组合控件兼顾了绘制效果,它本身是一个 LinearLayout,容纳的布局如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.baoyachi.stepview.HorizontalStepsViewIndicator
android:id="@+id/steps_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content""/>
<RelativeLayout
android:id="@+id/rl_text_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>

其中RelativeLayout负责容纳TextView控件, HorizontalStepsViewIndicator 负责绘制效果。

TapBarMenu

提示性的控件代表了着一种典型的自定义控件的实现思路,即在某个布局上添加自定义的布局。

像系统定义的 Toast,Dialog,甚至 Activity 都是在 Window 上添加自定义布局,SnackBar 则是在 CoordinatorLayout 布局上添加自定义布局,以便利用自定义的 Behavior 类。

在具体实现上,不同的控件根据实现效果还要考虑 IPC 过程,动画效果,触摸事件等。

Toast

Toast 是一个系统提供的提示性控件,本质和 PopupWindow 一样,是在 Window 上添加和移除 View,所以最重要的是设置 View,否则显示的时候将出现异常。它的使用分为两步

1.构造。

常用的 makeText 方法即构造了一个只包括 TextView 的布局,如果要设置文本,必须将其 id 设置为android.R.id.message。

1
2
3
4
5
6
7
8
9
10
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
Toast result = new Toast(context);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);

result.mNextView = v;
result.mDuration = duration;
return result;
}

2.显示。

1
2
3
4
5
6
7
8
9
10
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
tn.mNextView = mNextView;
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
service.enqueueToast(pkg, tn, mDuration);
}

显示的过程是一个 IPC 过程,这是为了避免与其他应用的 Toast 冲突。显示 Toast 依靠 Handler 来实现,本质是通过 WindowManager 服务添加 mNextView 来实现的。Toast 可以配置的参数都应用在WindowManager.LayoutParams 上。 包括 gravity,偏移量,margin等。

1
2
3
WindowManager mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
mParams.gravity = gravity;
mWM.addView(mView, mParams);

自定义View

因为可以设置 View,故而可以通过自定义 View 来实现各种效果,甚至可以通过 onAttachedToWindow 方法来获得动画效果。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
ValueAnimator va = ValueAnimator.ofInt(Color.GREEN, Color.CYAN).setDuration(1500);
va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
drawable.setColor((Integer) animation.getAnimatedValue());
}
});
va.start();
}

SnackBar

SnackBar 比 Toast 要轻量,它也要防止冲突,但并非通过 IPC 机制来完成。

首先是构建

1
public static Snackbar make(View view, int resId, @Duration int duration)

这里的 View 只用来寻找 Parent 布局,或者是 CoordinatorLayout 或者是 android.R.id.content,而后是加载 SnackbarLayout 到布局。

1
2
3
4
5
6
7
private final ViewGroup mTargetParent;
final SnackbarLayout mView;
private Snackbar(ViewGroup parent) {
mTargetParent = parent;
mView = (SnackbarLayout) inflater.inflate(
R.layout.design_layout_snackbar, mTargetParent, false);
}

这个 SnackbarLayout 是一个线性布局,它采用 LayoutInflate 将带 merge 标签的布局加载进自己

1
2
3
4
5
6
7
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView android:id="@+id/snackbar_text""/>

<Button android:id="@+id/snackbar_action"
android:textColor="?attr/colorAccent"
style="?attr/borderlessButtonStyle"/>
</merge>

这样 setAction 方法就是此 Button 点击事件的委托处理,点击将执行 dispatchDismiss 方法,

show 方法交给 SnackbarManager 来处理,这个类管理两个 SnackbarRecord 实例,以避免叠加

1
2
private SnackbarRecord mCurrentSnackbar;
private SnackbarRecord mNextSnackbar;

显示的时候,要取消当前,实例化和显示后者,SnackbarRecord 实际就是一个包装了功能的回调,其执行内容为 Callback 的 show 方法。真实的显示方法为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final void showView() {
if (mView.getParent() == null) {
final ViewGroup.LayoutParams lp = mView.getLayoutParams();
if (lp instanceof CoordinatorLayout.LayoutParams) {
CoordinatorLayout.LayoutParams clp = (CoordinatorLayout.LayoutParams) lp;
Behavior behavior = new Behavior();
behavior.setListener(new SwipeDismissBehavior.OnDismissListener());
clp.setBehavior(behavior);
clp.insetEdge = Gravity.BOTTOM;
}
mTargetParent.addView(mView);
//使用动画
}
}

这里 SnackbarLayout 将配置上 SwipeDismissBehavior,该类决定了其行为。SwipeDismissBehavior 是一个提供 swipe-to-dismiss 的类,依靠 ViewDragHelper 实现,

FloatingToolbar

FloatingToolbar 是模仿 Snackbar 实现的一个库。

如果将动画放慢,就会发现首先 Fab 先消失(alpha动画),而后 FloatingToolbar 才扩展出现(scaleX动画),因此主要的工作是设置和添加布局。

FloatingToolbar 是一个线性布局,可以添加自定义布局,也可以添加菜单。Fab 的点击事件发生后,将执行动画显示;如果再点击菜单中的控件,将执行各自ItemClick,并执行隐藏操作。

FloatingToolbar 的优点还在于考虑了与Snackbar 冲突的情况,通过添加 OnScrollListener 来在 RecyclerView 滑动时隐藏 FloatingToolbar 。

FloatingToolbar 需要在自定义布局,虽然有自由度,但没有 Snackbar 封装的那么好,而且也没有利用Behavior类。

Fab 的点击事件发生后,将执行动画显示;如果再点击菜单中的控件,将执行各自ItemClick,并执行隐藏操作。

Alert

与之类似还有LoadToast, 原理是在 Window 上添加控件,并使用动画来显示。

通知

服务 NotificationManager 将通知展示在状态栏处,属于跨进程通信。

其中 Notification 类是 Parcelable 对象,包装了通知所需的数据,包括关于信息的,关于布局的。

1
2
3
4
5
6
7
8
9
10
11
12
13
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Notification compat = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.boot)
.setContentText("通知必须有:标题,内容和显示在状态栏的Small图标")
.setContentTitle("标题")
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.beauty))
.setSubText("子标题")
.setCustomBigContentView(remoteViews) //大图
.setAutoCancel(true) //点击取消
.setTicker("通知") //显示在状态栏的标题
.setDefaults(Notification.DEFAULT_ALL) //效果
.build();
manager.notify("like", 6, compat);

通知必须有:标题,内容和显示在状态栏的Small图标

Drawable 是 Android 绘制机制的代表,很多时候实现绘制效果并不需要自定义控件,这里谈谈如何自定义 Drawable 以及如何产生动画效果

自定义Drawable的典型例子:FadeDrawable(Picasso)

Picasso 中加载完 Bitmap 后有一个渐显效果,这是通过自定义 FadeDrawable 来实现的,其简单逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FadeDrawable extends BitmapDrawable {
private long mStartTime = 0;
private long mDuration = 2000;
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
long time = System.currentTimeMillis() - mStartTime;
if(time<mDuration){
int alpha = (int) (255 * max(0, time) / mDuration);
setAlpha(alpha);
invalidateSelf();
} else {
setAlpha(255);
}
}
public void fade(){
mStartTime = System.currentTimeMillis();
invalidateSelf();
}
}

这样调用 fade() 方法后,在两秒内将有一个渐显得动画效果出现。

实际上这个效果还可以通过属性动画来实现

1
2
3
4
5
6
7
8
ValueAnimator va = ValueAnimator.ofInt(0, 255).setDuration(2000);
va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
bitmapDrawable.setAlpha((Integer) animation.getAnimatedValue());
}
});
va.start();

这是通过操纵 bitmapDrawable 对象来实现的,实际还可以直接操作 ImageView 控件来实现。

1
mImageView.animate().alpha(1f).withLayer().setDuration(2000).start();

这三种方法第三种间接,第一种封装性好,适合广泛性。

自定义 Drawable 的典型方法就是通过设置参数来控制绘制效果,既可以通过设置条件来终结重绘过程,亦可以直接使用属性动画。

PathDrawable 及动画

通过构建路径具备相当的灵活性,可以实现什么绘制效果主要取决于想象力。

应该注意到路径的初始化应该放在如下方法中

1
2
3
4
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
createPath();

其次记录路径的描述点最好使用二维数组

1
private float[][] as = new float[2][5];

这样可以便于构建对象的属性对象(Property)

1
Property<View, float[][]> mBProperty = new Property<View, float[][]>(float[][].class, "bs")

例如抽屉控件中的 DrawerArrowDrawable 就是一个典型的 PathDrawable,且有动画效果。它的绘制效果是绘制三条线,同时采用一个 process 参数(float)来控制线的起始位置和长度,以及旋转角度。其实现可以简化为

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
@Override
public void draw(Canvas canvas) {
Rect bounds = getBounds();
int width = bounds.width();
int height = bounds.height();
float midX = 0;
float midY = bounds.centerY();
float topY = lerp(midY - height / 3, midY, mProgress);
float botY = lerp(midY + height / 3, midY, mProgress);
float dx = lerp(width, width / 2, mProgress);
mPath.rewind();
// draw middle bar
mPath.moveTo(midX, midY);
mPath.rLineTo(width, 0);
// bottom bar
mPath.moveTo(midX, botY);
mPath.rLineTo(dx, midY + height / 3 - botY);
// top bar
mPath.moveTo(midX, topY);
mPath.rLineTo(dx, midY - height / 3 - topY);
mPath.close();
canvas.save();

float rotate = lerp(0, 270, mProgress); //将 Path 旋转,区间为[0, 270]
canvas.rotate(rotate, bounds.centerX(), bounds.centerY());
canvas.drawPath(mPath, mPaint);
canvas.restore();
}

这样绘制路径就被 mProcess 参数完全控制了,可以对其使用属性动画。

TextDrawable

TextDrawable 继承 ShapeDrawable,它之所以能够绘制文字和底边是使用 Paint 的结果,绘制文字的核心代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
Rect r = getBounds();
int count = canvas.save();
canvas.translate(r.left, r.top);
int width = this.width < 0 ? r.width() : this.width;
int height = this.height < 0 ? r.height() : this.height;
//字体尺寸不超过高度的一半
int fontSize = this.fontSize < 0 ? (Math.min(width, height) / 2) : this.fontSize;
textPaint.setTextSize(fontSize);
canvas.drawText(text, width / 2, height / 2 - ((textPaint.descent() + textPaint.ascent()) / 2), textPaint);
canvas.restoreToCount(count);
}

这里画笔的对齐方式设置成居中 Paint.Align.CENTER,绘制点在水平方向上位于半宽度,在垂直方向上需要得到 baselinde 的位置,而不是垂直居中的位置。

以字体尺寸为 24sp 的 Paint 而言,其 ascent 值为 -22.265625, descent 值为 5.859375,二者的差值即为从中线向下的偏移量。

该库的使用时应该有个有意思的点:

1.用两个 TextDrawable 合成 LayerDrawable,造成各占据一边的结果

1
2
3
4
5
Drawable[] layerList = {
new InsetDrawable(left, 0, 0, toPx(31), 0),
new InsetDrawable(right, toPx(31), 0, 0, 0)
};
return new LayerDrawable(layerList);

2.形成 AnimationDrawable 帧动画。

1
2
3
4
5
6
7
AnimationDrawable animationDrawable = new AnimationDrawable();
for (int i = 10; i > 0; i--) {
TextDrawable frame = builder.build(String.valueOf(i), mGenerator.getRandomColor());
animationDrawable.addFrame(frame, 1200);
}
animationDrawable.setOneShot(false);
animationDrawable.start();

这样实现的动画效果,实际并不聪明,浪费内存。

MaterialProgressDrawable(SwipeRefreshLayout)

MaterialProgressDrawable 是一个典型的循环动画 Drawable,

1
2
3
4
5
6
7
8
@Override
public void draw(Canvas c) {
final Rect bounds = getBounds();
final int saveCount = c.save();
c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY());
mRing.draw(c, bounds);
c.restoreToCount(saveCount);
}

从绘制方法上看,动画效果是通过rotate操作达成的,具体绘制委托给了 Ring 对象。

位置与尺寸

1.关于控件的位置,有left,translationX 和 X三种,他们都和 RenderNode 有关,计算公式为

1
2
3
public float getX() {    
return mLeft + getTranslationX();
}

为什么要在View中搞出这么多变量呢?

如果只变更 left,则只是左边界的变化,View 的视觉宽度将发生变化;TranslationX 则能够产生平移的效果,它同时附加在 left 和 right 上。

实际上同时移动 left 和right 相同的距离,也能达到平移的效果,类似于ViewCompat.offsetLeftAndRight 造成的效果,但这个与 TranslationX 实现的平移效果有所区别。

实际上同时移动 left 和right 相同的距离,也能达到平移的效果,类似于 ViewCompat.offsetLeftAndRight 造成的效果,但这个与 TranslationX 实现的平移效果有所区别。

前者只是非常机械的渲染效果,只要碰上 requestLayout 方法就复原了,后者的平移在重新布局后仍然有效。而且只变更left不会考虑一些类似于居中的要求,控件在变化过程中往往就“失真’’了。

如果确实要改变控件的宽度,应该从 LayoutParams.width 入手,修改此值,并调用方法,控件会重新进行测量。

2.再来说说 mScrollX/mScrollY ,它们只影响内容的绘制,不会影响背景 Drawable 的绘制,实际是通过对内容区域加偏移造成,最终的绘制效果是偏移后的内容区域与原本区域的。

1
2
3
4
5
6
public void invalidate(Rect dirty) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
invalidateInternal(dirty.left - scrollX, dirty.top - scrollY,
dirty.right - scrollX, dirty.bottom - scrollY, true, false);
}

3.此外还可以使用动画来改变控件位置,属性动画就不提了,值得一提的是补间动画,默认的四种补间动画采用Transformation里的矩阵来操作渲染结果,但并不真的改变属性,如果要达成属性动画的效果,可以利用下面的方法

1
2
public void setFillBefore(boolean fillBefore);
public void setFillAfter(boolean fillAfter);

最后评价下各种操作的性能

  • left 虽然是RenderNode实现,但需要 CPU 更新 displayList,调用和递归多(56/1022),GPU监视上蓝色部分长
  • translationX 基本无更新 displayList 的调用和递归存在,蓝线几乎没有,耗时极少
  • LayoutParams 除非测量事件,耗时验证
  • offset ,耗时极少
  • animation 更新 displayList极多
  • animtor 不更新 displayList,快

layout_* 机制与 LayoutParams

layout*机制通过内部类 LayoutParams 中定义以 layout为前缀的属性供子控件使用,从而控制子控件的行为,这些属性并不为布局类自己所使用。以AppBarLayout为例

1
2
3
4
5
<android.support.design.widget.AppBarLayout>

<android.support.v7.widget.Toolbar
app:layout_scrollFlags="scroll|enterAlways" />
</android.support.design.widget.AppBarLayout >

Toolbar 本身并没有 layoutscrollFlags 属性,该属性定义在类 AppBarLayout.LayoutParams 中,加前缀 layout表示这是一个父布局定义而子控件使用的属性。虽然这个属性是由容器来读取 XML文件进行解析和实例化的。

最广泛最著名的属性来自于 ViewGroup.LayoutParams 中的属性:layout_width 与 layout_height, 是容器用来约束子控件的宽度和高度的,ViewGroup.MarginLayoutParams 增加了6个margin属性,如果采用这种 LayoutParams,就能够给子控件设置 layout__margin属性,容器会在布局时利用这个属性。

自定义 LayoutParams 需要覆盖以下4个方法,这些方法是为子控件生成 LayoutParams 对象所必须的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}

实际上布局每添加一个控件,都会采用 generateDefaultLayoutParams() 方法来生成一个 LayoutParams 对象应用在子控件上(见addView方法)。

如果默认产生的 LayoutParams 对象不能通过 checkLayoutParams 方法的检查,则使用 generateLayoutParams 方法来产生 LayoutParams 对象。

因此自定义LayoutParams 时要故意使得 generateDefaultLayoutParams 产生的对象通不过检查,而使用 generateLayoutParams(AttributeSet attrs)产生的 LayoutParams 对象。

约定 LayoutParams 中解析的属性值均要以 layout_ 开头。

尺寸测量

控件的尺寸测量实际比想象的复杂,我们先看系统的默认实现。对于容器而言,除了测量自身,还要考虑测量子控件,这一步往往是调用 measureChild 方法来完成的

1
2
3
4
5
6
7
8
9
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

在真正的测量方法前,将对复合尺寸进行重新计算,以求得实际的控件尺寸。其方法如下

1
2
3
4
5
6
7
8
9
10
11
//spec 的复合尺寸,padding 是控件的留白
//childDimension 是赋给LayoutParams的尺寸,是控件想达到的尺寸,可能是 -1 -2 或 具体数值
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec); // 容器的尺寸模式
int specSize = MeasureSpec.getSize(spec); // 容器的尺寸大小
int size = Math.max(0, specSize - padding); // 容器的尺寸大小 - 控件的留白 == 控件在容器中的最大尺寸
int resultSize = 0;
int resultMode = 0;
// 根据 LayoutParams 和 MeasureSpec 确定最终的复合尺寸
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

控件最终的复合尺寸受到两个因素的影响,即控件自定义的 LayoutParams 和容器的 MeasureSpec。其结果如下表所示

Spec\Params MATCH_PARENT WRAP_CONTENT 具体数值(NUM)
EXACTLY EXACTLY + size AT_MOST + size EXACTLY+ NUM
AT_MOST AT_MOST + size AT_MOST + size EXACTLY + NUM
UNSPECIFIED UNSPECIFIED + size UNSPECIFIED + size EXACTLY + NUM

实际默认的容器 SpecMode 往往是 EXACTLY,因此关注第一行。子控件设置为 match_parent 和 具体数值时,都将获得 exactly模式,不过尺寸有所差别而已,而 warp_content 的情况较为复杂,其模式为 at_most,尺寸想达到容器尺寸,需要进一步处理。

而后就是控件自身的测量方法

1
2
3
4
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

默认情况下的最小尺寸为背景 drawable 的尺寸 ,要在所得的尺寸和这个最小尺寸间做出决定,即 getDefaultSize 方法

1
2
//size 是最小尺寸,measureSpec 是复合尺寸
public static int getDefaultSize(int size, int measureSpec) {}

getDefaultSize 方法将根据复合尺寸的模式得到最终在最小尺寸和复合尺寸间做出选择。

specMode UNSPECIFIED AT_MOST EXACTLY
最终尺寸 size specSize specSize

即仅在容器指定尺寸模式为 UNSPECIFIED 的情况下使用最小尺寸,其余情况可以默认这一步不存在。

这里有一个问题如果控件采用 warp_content,那么到这一步的 specSize 实际是容器的尺寸。这说明默认情况下处理 warp_content 是不合理,自定义控件直接继承View时要注意。

最后就是为测量尺寸 mMeasuredWidth 和 mMeasuredHeight 赋值。

1
2
3
4
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
}

resolveSizeAndState

刚才说了默认的 measureChild 在碰到子控件设置了 warp_content 时会取容器尺寸的问题,除非子控件不采用默认的自测量方式。这个问题可以用 resolveSizeAndState 方法修复。

1
2
3
4
5
6
/**
* @param size How big the view wants to be.
* @param measureSpec Constraints imposed by the parent.
* @param childMeasuredState Size information bit mask for the view's children.
*/
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState);
specMode AT_MOST EXACTLY UNSPECIFIED
最终尺寸 Min(specSize, size) specSize size

resolveSizeAndState 方法和 getDefaultSize 方法不同之处在于如何解析 AT_MOST,这是关键之处。

即将默认的测量方法改为

1
2
3
4
5
6
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
resolveSizeAndState(getSuggestedMinimumWidth(), widthMeasureSpec, MEASURED_STATE_MASK),
resolveSizeAndState(getSuggestedMinimumHeight(), heightMeasureSpec, MEASURED_STATE_MASK));
}

自定义测量方法

如果想达到一些其它效果,如在 DrawLayout 中要求内容控件占据容器,实际上在 LayoutParams 上的设置已无任何意义,测量时完全不予考虑,这时就要求抛弃默认的测量方法,去自定义测量方法。

自定义测量方法y要依赖控件的自测量方法,其核心在于在合适的时机改造复合尺寸。

例如要求一个控件占据容器是最容易的情况,此时完全不用考虑容器Spec模式和 LayoutParams 参数。

1
2
3
4
5
6
mContent.measure(
MeasureSpec.makeMeasureSpec(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(
getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)
);

在控件内部首先对传入的尺寸进行改造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
switch (MeasureSpec.getMode(widthSpec)) {
case MeasureSpec.EXACTLY:
// Nothing to do
break;
case MeasureSpec.AT_MOST:
widthSpec = MeasureSpec.makeMeasureSpec(
Math.min(MeasureSpec.getSize(widthSpec), mMaxWidth), MeasureSpec.EXACTLY);
break;
case MeasureSpec.UNSPECIFIED:
widthSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY);
break;
}
// Let super sort out the height
super.onMeasure(widthSpec, heightSpec);
}

最要命的情况是要同时处理容器Spec模式和 LayoutParams 参数共九种情况,不过好在遵循 getChildMeasureSpec 方法的套路就行。

布局机制

布局机制比测量机制要简单地多,但应该注意到布局时会掉用测量过程,此外有时子控件如何布局与 LayoutParams 参数有关,布局时要考虑 padding 和 margin 的影响,获取控件宽度不应该用 LayoutParams.width (可能为负值) 等等。

触摸机制

触摸机制首先从 Activity 开始,而后有ViewRootImpl传递下去

1
2
3
4
5
6
public boolean dispatchTouchEvent(MotionEvent ev) {
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}

默认的处理流程是一个来回:一半是事件分发(dispatchTouchEvent方法),一半是事件处理(onTouch 方法)。其中容器 ViewGroup 和控件 View 的分发方法是不一致的,前者会使用 onInterceptTouchEvent 方法拦截事件,且默认情况下不拦截,逐层交给子控件分发,直到根View;控件 View 的分发会调用事件处理,且会逐级上溯,直到返回 Activity 。

假设布局层级是 TopLayout,BottomLayout 和 CircleImage,则默认情况下流程是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//1.向下传递拦截事件
TopLayout onInterceptTouchEvent: false 0
BottomLayout onInterceptTouchEvent: false 0
//2.向上传递处理事件
CircleImage onTouchEvent: false 0
CircleImage dispatchTouchEvent: false 0
BottomLayout onTouchEvent: false 0
BottomLayout dispatchTouchEvent: false 0
TopLayout onTouchEvent: false 0
TopLayout dispatchTouchEvent: false 0
MainActivity onTouchEvent: false 0
MainActivity dispatchTouchEvent: false 0
//3.无人处理
MainActivity onTouchEvent: false 2
MainActivity dispatchTouchEvent: false 2

默认情况下整个流程是不进行拦截的处理的,所以流程会从顶到根走一个来回,但仅能处理 Action_down 的情况,后续的其它动作如Action_Move 等将停留在Activity 中。

如果在 BottomLayout 中截断事件,这里有几种情况。只拦截不处理和默认情况一样。同时拦截和处理结果如下

1
TopLayout onInterceptTouchEvent: false ACTION_DOWNBottomLayout onInterceptTouchEvent: true ACTION_DOWN //拦截方法只能执行一次BottomLayout onTouchEvent: true ACTION_DOWNBottomLayout dispatchTouchEvent: true ACTION_DOWNTopLayout dispatchTouchEvent: true ACTION_DOWNMainActivity dispatchTouchEvent: true ACTION_DOWNTopLayout onInterceptTouchEvent: false ACTION_MOVE  //父布局的拦截方法倒是能多次执行BottomLayout onTouchEvent: true ACTION_MOVEBottomLayout dispatchTouchEvent: true ACTION_MOVETopLayout dispatchTouchEvent: true ACTION_MOVE //消灭这一行,禁止 TopLayout 拦截MainActivity dispatchTouchEvent: true ACTION_MOVETopLayout onInterceptTouchEvent: false ACTION_UPBottomLayout onTouchEvent: true ACTION_UPBottomLayout dispatchTouchEvent: true ACTION_UPTopLayout dispatchTouchEvent: true ACTION_UPMainActivity dispatchTouchEvent: true ACTION_UP

即流程到 BottomLayout 这一层被截断了,如果要 TopLayout 不进行拦截,可以使用下列方法

1
getParent().requestDisallowInterceptTouchEvent(true);

如果不拦截,只处理结果如下

1
TopLayout onInterceptTouchEvent: false 0BottomLayout onInterceptTouchEvent: false 0CircleImage onTouchEvent: false 0CircleImage dispatchTouchEvent: false 0BottomLayout onTouchEvent: true 0BottomLayout dispatchTouchEvent: true 0TopLayout dispatchTouchEvent: true 0MainActivity dispatchTouchEvent: true 0TopLayout onInterceptTouchEvent: false 2BottomLayout onTouchEvent: true 2BottomLayout dispatchTouchEvent: true 2TopLayout dispatchTouchEvent: true 2MainActivity dispatchTouchEvent: true 2TopLayout onInterceptTouchEvent: false 1BottomLayout onTouchEvent: true 1BottomLayout dispatchTouchEvent: true 1TopLayout dispatchTouchEvent: true 1MainActivity dispatchTouchEvent: true 1

区别是下面的子控件还能得到一次处理的机会, 否则即便子控件设置了处理方法,也不会得到执行机会。当然如果子控件不处理,那么以后依然将会被屏蔽。

监听器是事件处理的特殊情况,如果配置了监听器,等同于设置了事件处理。如果容器和子控件都配置了监听器,那么按照事件处理的顺序是子控件优先截断控制权;容器如果想夺回控制权,只能使用拦截方法进行拦截。

修改触摸机制

修改触摸机制的核心是确定那个控件在哪种情况下能够进行拦截和消费事件,这样使得不同的控件都有机会处理触摸事件。

先说下可以着手的三个地方

  • onInterceptTouchEvent 方法:返回 true,则事件不再向下分发,即屏蔽了子控件,使得子控件设置的点击事件失效,因此要谨慎使用。此外如果容器消费了事件,该方法只执行一次。
  • onTouchEvent 方法:返回 true,则此后的处理流程就到这一层。
  • dispatchTouchEvent 方法:返回 true,此时不管*onTouchEvent 方法如何,都能形成闭合流程。

例如我们要求在 TopLayout 中拦截左右滑动,在 BottomLayout 中拦截 上下滑动,同时也要保证 CircleImage 能够响应点击事件。

三个控件都要能拦截事件,这就要求它们各自精确拦截自己的那一部分。

首先给 CircleImage 设置一个监听器,此时事件是到 CircleImage 这里得到处理的,但TopLayout 和 BottomLayout 还有拦截事件的机会。

再来改造 TopLayout ,前提是不能拦截 ACTION_DOWN 和 ACTION_UP,这样就屏蔽了子控件的点击监听,这两个动作里只能做一些初始化和清理的工作。而后在 ACTION_MOVE 中进行拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean isIntercept = false;
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
isIntercept = false;
break;
case MotionEvent.ACTION_MOVE:
if ("左右滑动") {
isIntercept = true;
}
break;
case MotionEvent.ACTION_UP:
default:
isIntercept = false;
break;
}
return isIntercept;
}

还有在 onTouchEvent 方法里也不能在 ACTION_DOWN 和 ACTION_UP 里进行事件的处理,这样也会导致子控件的点击事件失效。只能在 ACTION_MOVE 进行相同条件下的处理,这样才能构成闭环处理流程。

最后是 BottomLayout ,和TopLayout 类似,只需要改一下拦截和处理的条件。

这里三个控件都要获得拦截事件的机会,底层的控件尤其重要,它要求响应点击事件的特性使得父控件无法拦截和处理 ACTION_DOWN 和 ACTION_UP。父控件之间更是不得不小心翼翼,避免彼此之间的冲突。

实践:点击控件带扩张效果

ElasticViews

要达成这样的效果,且要求保留子控件的点击事件,实际上不需要进行事件的拦截,只需要在 dispatchTouchEvent 中添加一个动画即可。

1
2
3
4
5
6
7
8
9
10
11
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
//只处理 ACTION_UP,防止执行多个动画,造成View参数紊乱
if(event.getAction() == MotionEvent.ACTION_UP) {
ViewCompat.animate(view).setDuration(duration)
.scaleX(scale).scaleY(scale).setInterpolator(new CycleInterpolator(0.5f))
.withLayer()
.start();
}
return super.dispatchTouchEvent(event);
}

这里要注意的是如果容器即其子控件不设置点击事件监听器,那么可以不加第一行,因为此时dispatchTouchEvent只执行一次,在ACTION_DOWN之后就被屏蔽了;

然而这过于理想了,一旦有一个子控件拦截的事件,将造成多个动画同时执行,View的scale参数将紊乱。因此为了安全,需要添加第一行。

MotionEvent和手势

动作包含动作码和坐标集合两个部分,前者表示是按下还是离开等;此外多触摸屏幕还可以响应多个手指,其中只能有一个起作用的,即常见的 mActivePointerId。

1
2
3
4
5
6
7
8
final int action = MotionEventCompat.getActionMasked(event);
final int actionIndex = MotionEventCompat.getActionIndex(event);
int pointerCount = event.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
int pointerId = event.getPointerId(i);
float x = event.getX(actionIndex);
float y = event.getY(actionIndex);
}

手势库:Sensey

滚动机制(Scroller)

scrollTo 方法就能够使得控件内容移位,其原理是改变参数 mScrollX/mScrollY ,这两个数值会在绘制的时候移动绘制区域,这是滚动机制的基础。

scrollTo 方法的问题在于滚动花费的时间太短,Scroller 通过拉长这个时间带来平滑的视觉效果,具体做法是将整个过程拆分成若干的步进过程,逐步改变 mScrollX/mScrollY 的数值。

Scroller 类本身是一个纯属性类,只需要传入滑动时间,起始值等描述一次滑动过程的必要参数。但它并不能直接滑动控件,想要滑动控件需要与具体控件配合驱动步进计算。Scroller类的手动更新方法是

1
2
3
4
5
6
7
8
9
10
11
12
13
//1.启动滚动过程
mScroller.startScroll(getScrollX(), getScrollY(), 100, 100);
invalidate();
//2.自我驱动
@Override
public void computeScroll() {
if (mScroller != null) {
if (mScroller.computeScrollOffset()) { //判断是否完成
scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); //实现滚动的一小步
setAlpha(1f*scroller.getCurrX()/scroller.getFinalX());//附加效果
postInvalidate(); //实现滚动循环
}
}

这里达成真实的滚动效果是靠改变控件 mScrollX/mScrollY 位置来产生的,完全可以考改变其它属性来达成其它动画效果。

为了兼容性,可以使用类ScrollerCompat代替实现,该类还提供了fling 和 springback 两种滑动方式。

绘制机制与动画机制

View 的实际绘制区域与布局区域是不一致的,它与 mScrollX/mScrollY 有关

1
2
3
4
5
6
public void getDrawingRect(Rect outRect) {    
outRect.left = mScrollX;
outRect.top = mScrollY;
outRect.right = mScrollX + (mRight - mLeft);
outRect.bottom = mScrollY + (mBottom - mTop);
}

在 onDraw 方法上添加附加效果是一种常用的手段。

View 可以执行三种动画

1.补间动画

1
public void startAnimation(Animation animation)

2.状态转移动画

1
public void setStateListAnimator(StateListAnimator stateListAnimator);

3.属性动画

1
public ViewPropertyAnimator animate();

EdgeEffect

EdgeEffect 是用来绘制边界阴影的,它的实质是画一个弧顶过某边的圆,同时截取弧顶部分。默认情况下绘制的顶边。

通过 onPull, onAbsorb 方法,EdgeEffect 可以控制弧顶漏出的比例,这是纯属性设置的方法,必须刷新绘制才能生效。

1
public void onPull(float deltaDistance, float displacement)

通过 onRelease 方法可以产生回弹,这与Scroll 自我驱动更新的原理是一样的

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
edge.setSize(getWidth(), getHeight());
if (!edge.isFinished()) {
final int restoreCount = canvas.save();
if(edge.draw(canvas)){
postInvalidate();
}
canvas.restoreToCount(restoreCount);
}
}

在实际的使用中,应该给每一个边设置一个 EdgeEffectCompat , 这需要移位,旋转等操作 。以右边为例

1
2
3
canvas.rotate(90); //画布旋转90度,意味着坐标轴也偏移了90度,
canvas.translate(0,-getWidth()); //此时 y 轴实际是向左,故而反向回退一个宽度,矫正弧顶矩形
edge.setSize(getHeight(), getWidth());

左边

1
2
3
canvas.rotate(270); //画布旋转270度,此时y轴向右
canvas.translate(-getHeight(), 0);//弧顶矩形需要下移
edge.setSize(getHeight(), getWidth());

下边

1
2
3
canvas.rotate(180);
canvas.translate(-getWidth(), -getHeight());
edge.setSize(getWidth(), getHeight());

SurfaceView

SurfaceView 在一个独立的线程中进行绘制,不在主线程中不会占用主线程资源,一方面可以实现复杂而高效的UI,另一方面又不会导致用户输入得不到及时响应。由于应用程序的主线程除了要绘制UI之外,还需要及时地响应用户输入,否则的话,系统就会认为应用程序没有响应了,因此就会弹出一个ANR对话框出来。对于一些游戏画面就不适合在应用程序的主线程中进行绘制。这时候就可以使用 SurfaceView 。一个Surface绘图示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SurfaceView surfaceView = (SurfaceView) findViewById(R.id.surfaceview);
//1. 获取 SurfaceView 对象内部类SurfaceHolder
SurfaceHolder holder = surfaceView.getHolder();
//2. SurfaceHolder对象添加Callback接口,执行在UI线程
holder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder)

//4.Surface设置参数被改变将调用此回调方法,实现主要绘制逻辑
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
//1. 通过锁定获取canvas对象,开始编辑底层像素
Canvas canvas = holder.lockCanvas();
canvas.drawColor(0x4398ff);
//2. 消除canvas对象,结束像素编辑
holder.unlockCanvasAndPost(canvas);
//3. 再次锁定并取消锁定
canvas = holder.lockCanvas(new Rect(0, 0, 0, 0));
holder.unlockCanvasAndPost(canvas);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder)
});

SurfaceView 有两个子类GLSurfaceViewVideoView,可见视频图像亦是靠Surface机制来渲染。SurfaceView是一个专门负责绘制Surface内容的View对象,它最大的优点是使用单独线程完成绘制工作。

通常每个窗口背后对应一个Surface,不同Surface的Z序列不同,在底层将这些Surface的内容合成;Surface可以理解成绘制内容数据。

绘制工作主要是由SurfaceHolder来完成的。SurfaceHolder负责与 Surface 内容数据打交道,可以控制比如Surface尺寸,格式,像素并监听Surface的变化。

一般而言,SurfaceView 进入前台时 Surface 内容被创建,转入后台则被销毁;这是Android系统内存管理的特征;在 Surface 内容被创建后可以通过 holder.lockCanvas(Rect dirty); 获得一个 canvas 对象,对 dirty 矩形区域内进行绘制;此时脏区内的 Surface 内容被锁定是线程安全的;使用 unlockCanvasAndPost(canvas) 方法释放锁定后,脏区内容将被系统渲染展示到屏幕上。但数据内容没有被清除;因此如果更改了绘制区域后,将绘制先前内容。因此使用holder.lockCanvas(new Rect(0, 0, 0, 0)) 锁定并取消锁定的方法清除内容。

ViewTreeObserver

ViewTreeObserver 接口可以响应视图树的变化,其中最重要的一个接口就是 OnPreDrawListener

1
2
3
public interface OnPreDrawListener {
public boolean onPreDraw();
}

这个接口之所以重要在于它的执行时机非常好,处在布局之后,绘制之前,各项参数(如尺寸和位置)均已确定,正是使用各种 trick 的方法。

此外一个使用较多的接口是 OnGlobalLayoutListener ,执行时间在控件发生 visibility 的变化。

各种子控件

Space 与 ViewStub

Space 是一个典型的占位控件,它始终处于 INVISIBLE 状态,参与测量和布局,但 draw 方法为空。

ViewStub 则处于 GONE 状态,这意味着它没有尺寸, draw 方法也为空。 直到执行 inflate() 方法载入其它控件

1
2
ViewStub stub = (ViewStub) findViewById(R.id.stub);
View inflated = stub.inflate();

ViewStub 会将子布局加载到它的父布局中去,新的子布局将继承原 ID,同时让父布局移除自己。

FrameLayout

FrameLayout 测量时以子控件的最大高度/宽度作为自己的尺寸。它的 LayoutParams 多了一个参数,并利用这个参数来对齐

1
public int gravity = UNSPECIFIED_GRAVITY;

在布局方法中根据子控件的 gravity 参数来对齐子控件

1
2
3
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

absoluteGravity 决定了子控件的 left 位置,verticalGravity 决定了子控件的 top 位置,计算如下

1
2
3
4
5
6
7
8
9
10
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
case Gravity.RIGHT:
childLeft = parentRight - width - lp.rightMargin;
case Gravity.LEFT:
childLeft = parentLeft + lp.leftMargin;
}
child.layout(childLeft, childTop, childLeft + width, childTop + height);

ViewAnimator

ViewAnimator 是 FrameLayout 的子类,在切换布局时能够执行动画。这种效果是重写 addView 和 removeView 方法来实现的,其子类有 TextSwitcher,ImageSwitcher 和 ViewFlipper。

ViewFlipper 能够自动播放,这是通过在 onAttachedToWindow 方法中启动线程循环达成的,有意思的是它还使用广播接收器处理了屏幕熄灭和用户划开屏幕保护锁的广播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
final IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_USER_PRESENT);
getContext().registerReceiverAsUser(mReceiver, android.os.Process.myUserHandle(),
filter, null, getHandler());
if (mAutoStart) {
startFlipping();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mVisible = false;
getContext().unregisterReceiver(mReceiver);
updateRunning();
}

HorizontalScrollView

HorizontalScrollView 是一个继承 FrameLayout 的水平滚动控件,只应有一个子控件,它的滚动被称之为覆盖滚动(OverScroll),需要处理容器自身的尺寸小于它所容纳的子控件的尺寸的情况。

覆盖滚动有三种类型

  • OVER_SCROLL_NEVER 子控件永远被束缚在容器内
  • OVER_SCROLL_ALWAYS 子控件永远能够滚动出容器外
  • OVER_SCROLL_IF_CONTENT_SCROLLS 只有子控件大于容器才能发生

1.测量方法:容器只处理水平滚动,在垂直方向上倾向于将子控件完全扩展,且以自身容器宽度作为子控件宽度。如果不想这么做,可以设置参数 FillViewport 为 false。

2.拦截方法: 容器根据控件是否处于拖动状态决定是否拦截,只有处在MotionEvent.ACTION_MOVE 状态,滑动距离足够且触摸点在子控件内可以拦截此事件。发生拦截后,在事件处理方法中完成覆盖滑动。这一过程本质上是通过改变 mScrollX/mScrollY 的位置来实现的。

a.是否能够进行覆盖滑动,由滑动方式和滑动范围决定。

1
2
3
int range = getScrollRange();//计算滑动范围int 
overscrollMode = getOverScrollMode();//计算覆盖滑动方式
boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);

b.覆盖滑动范围为子控件宽度与容器宽度的差值,计算如下

1
Math.max(0,child.getWidth() - (getWidth() - mPaddingLeft - mPaddingRight);

滑动范围为子控件的宽度-容器的内容宽度,而且如果此值为负,就无法滑动。因此只有子控件的宽度大于容器宽度才能产生滑动,这也是判断子控件能否滚动的依据。

c.覆盖滑动的行为由 overScrollBy 完成

1
2
//mOverscrollDistance 指的是触发边界效应的距离
overScrollBy(deltaX, 0, mScrollX, 0, range, 0, mOverscrollDistance, 0, true)

该方法采用 Scroller 来进行滑动,会修正新的 ScrollX 和 ScrollY 值,。

4.翻页滑动全页滑动

翻页滑动(pageScroll),点击 (shift+)space可以调用,这里的页宽就是容器的宽度,如果向左翻页
则左边界为Math.max(0,getScrollX() - width),右边界为right = mTempRect.left + width

全页滑动(fullScroll 包括 arrowScroll),点击 (alt+)Pad key可以调用,如果向左翻页,左边界为0,右边界为width,即容器宽度。如果向右翻页,左右边界确定如下

1
2
3
View view = getChildAt(0);
mTempRect.right = view.getRight();
mTempRect.left = mTempRect.right - width;

布局容器动画:android.transition

Transition 是布局容器发生变化(包括其内部的控件发生变化:如添加控件,某个控件改变颜色)时应用的一种动画。

Transition 的简单调用如下

1
TransitionManager.beginDelayedTransition(sceneRoot);//默认采用A utoTransition

也可以使用 Scene

1
2
Scene scene = new Scene(sceneRoot, textView);
TransitionManager.go(scene, new Fade(Fade.IN));

此时实际并不能执行动画,只有当布局发生变化时才会执行,且一次设置只能执行一次动画发生的准确时间是在onPreDraw方法中。

Scene

首先来看Scene类,Scene 定义如下,它需要一个已经存在的容器(mSceneRoot),和一个新的控件或布局(mLayoutId或mLayout)。

1
2
3
4
5
6
7
public final class Scene {
private Context mContext;
private int mLayoutId = -1; //待加入到根布局中的布局,必须没有父控件
private ViewGroup mSceneRoot; //根布局,必须已经存在
private View mLayout; // alternative to layoutId
Runnable mEnterAction, mExitAction;
}

进入场景(Scene )的执行方法 enter 如下,实际是将容器的控件清空,添加新控件并将Scene对象自身存储进容器中。

1
2
3
4
5
6
7
8
9
public void enter() {
getSceneRoot().removeAllViews();

mSceneRoot.addView(mLayout);
LayoutInflater.from(mContext).inflate(mLayoutId, mSceneRoot);
mEnterAction.run();

setCurrentScene(mSceneRoot, this);
}

根布局对象 sceneRoot 中的 Tag 对象是一个SparseArray集合,用 layoutId 作为键 新创建的 Scene 对象存储进去,反过来也能从中获取Scene对象。

1
2
3
static Scene getCurrentScene(View view) {
return (Scene) view.getTag(com.android.internal.R.id.current_scene);
}

这样原本的根布局就埋下了所需的条件,当时机成熟时将被调用,当Transition动画播放完成后将被清除

TransitionManager

TransitionManager 是使用 Transition 的调度器,它将 Scene 和 Transition 匹配到一起,这样当某个 Scene 进入时,可以执行对应的 Transition。

1
2
3
4
5
6
7
public class TransitionManager {
private static Transition sDefaultTransition = new AutoTransition();
ArrayMap<Scene, Transition> mSceneTransitions = new ArrayMap<Scene, Transition>();
ArrayMap<Scene, ArrayMap<Scene, Transition>> mScenePairTransitions =
new ArrayMap<Scene, ArrayMap<Scene, Transition>>();
private static ArrayList<ViewGroup> sPendingTransitions = new ArrayList<ViewGroup>();
}

也可以指定离开某Scene时的 Transition 集合,由集合mScenePairTransitions保存,其 key 是进入场景 toScene。

1
2
3
4
public void setTransition(Scene fromScene, Scene toScene, Transition transition) {
ArrayMap<Scene, Transition> sceneTransitionMap = mScenePairTransitions.get(toScene);
sceneTransitionMap.put(fromScene, transition);
}

虽然 TransitionManager 保存了 Scene 和 Transition 的对应关系,但如何根据Scene获取Transition 还与所配置的 ViewGroup有关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//这里Scene表示进入场景,而 sceneRoot 中存储的是当前场景,即将要离开的场景,在播放完成后要被删除;
//因此先获取的是离开场景,离开Transition播放完成后将获取进入场景。
private Transition getTransition(Scene scene) {
Transition transition = null;
ViewGroup sceneRoot = scene.getSceneRoot();
if (sceneRoot != null) {
Scene currScene = Scene.getCurrentScene(sceneRoot);
if (currScene != null) {
ArrayMap<Scene, Transition> sceneTransitionMap = mScenePairTransitions.get(scene);
if (sceneTransitionMap != null) {
transition = sceneTransitionMap.get(currScene);
if (transition != null) {
return transition;
}
}
}
}
transition = mSceneTransitions.get(scene);
return (transition != null) ? transition : sDefaultTransition;
}

TransitionManager 调用动画的执行过程如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//1.准备工作1:清理先前的 Transition,包括 TransitionManager 和 sceneRoot 内部存储的两部分
ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);
for (Transition runningTransition : runningTransitions) {
runningTransition.pause(sceneRoot);
}
Scene previousScene = Scene.getCurrentScene(sceneRoot);
previousScene.exit();
//2.准备工作2:新的 Transition 获取初始值,并清理掉 sceneRoot内部存储的 Scene 对象
transition.captureValues(sceneRoot, true);
Scene.setCurrentScene(sceneRoot, null);
//3.在视图树中埋点
sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener);
mTransition.captureValues(mSceneRoot, false);
mTransition.playTransition(mSceneRoot);

当视图树发生变化时将执行 OnPreDraw 方法,此时就是执行动画的时机

1
2
3
4
5
6
7
//1.清理监听器和 Transition 集合
removeListeners();
sPendingTransitions.remove(mSceneRoot);
//2.此时布局过程已经完成,绘制还没有开始,是 Transition 获取终止值的绝佳时机。
mTransition.captureValues(mSceneRoot, false);
//3.起始值均已捕获,执行动画的条件已经成熟。
mTransition.playTransition(mSceneRoot);

这里 Transition 的执行实际是放在ViewTreeObserver.OnPreDrawListener接口中进行的,以便容器 mSceneRoot 重绘前调用,在播放完成后还要进行对应清理工作。

Transition

Transition 动画的数据结构是 TransitionValues,包含了待变化的属性字典

1
2
3
4
5
public class TransitionValues {
public View view;
public final Map<String, Object> values = new ArrayMap<String, Object>();
final ArrayList<Transition> targetedTransitions = new ArrayList<Transition>();
}

Transition 中的核心就是两个 TransitionValues 集合,表示动画的起始值。

1
2
ArrayList<TransitionValues> mStartValuesList; // only valid after playTransition starts
ArrayList<TransitionValues> mEndValuesList; // only valid after playTransitions starts

在 playTransition 方法中,首先要构建这两个集合

1
2
public abstract void captureStartValues(TransitionValues transitionValues);
public abstract void captureEndValues(TransitionValues transitionValues);

而后是遍历 sceneRoot,为每个 TransitionValues 对创建属性动画。

1
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,TransitionValues endValues)

Transition 的子类要定义如何获取起始值以及如何创建属性动画上,这都与具体的属性有关。

下面通过子类实例说说如何定义 Transition。

Visibility

Visibility 类能够响应控件可见性的变化, 这与他定义的三个属性有关,这些属性的数值完全由 View 提供,其名称和类型为

1
2
3
4
5
6
//1.表示 View 的可见性,int类型
static final String PROPNAME_VISIBILITY = "android:visibility:visibility";
//2.表示 View 的 Parent,View类型
static final String PROPNAME_PARENT = "android:visibility:parent";
//3.表示 View 的屏幕位置,int[]类型
static final String PROPNAME_SCREEN_LOCATION = "android:visibility:screenLocation";

这样在获取属性时,就可以将这些属性值写入属性字典中去

1
2
3
4
5
6
7
8
private void captureValues(TransitionValues transitionValues, int forcedVisibility) {
int visibility = transitionValues.view.getVisibility();
transitionValues.values.put(PROPNAME_VISIBILITY, visibility);
transitionValues.values.put(PROPNAME_PARENT, transitionValues.view.getParent());
int[] loc = new int[2];
transitionValues.view.getLocationOnScreen(loc);
transitionValues.values.put(PROPNAME_SCREEN_LOCATION, loc);
}

创建动画的方法委托给了子类,以 Fade 为例,它添加了一个透明度属性(由view.getAlpha()提供)

1
static final String PROPNAME_ALPHA = "fade:alpha";

创建关于 View透明度的属性动画,变化区间从当前透明度到 1。

1
2
3
4
5
6
@Override
public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues,
TransitionValues endValues) {
float startAlpha = getStartAlpha(startValues, 0);
return ObjectAnimator.ofFloat(view, View.ALPHA, startAlpha, 1);
}

同理子类 Scale 创建了关于 scaleX 和 scaleY 的属性动画,子类 Slide 创建了关于 TRANSLATION_X 和 TRANSLATION_Y的属性动画,在确立起始点时要考虑屏幕位置,它使用的属性动画要建立 Path,通过改变Path可以改变滑动的轨迹

1
ObjectAnimator.ofFloat(view, View.TRANSLATION_X, View.TRANSLATION_Y, path);

其他Transition

类 ChangeBounds 将改变 View 的 Bound 属性,同时也要处理父容器的 Bound 改变。它有如下属性

1
2
3
4
5
private static final String PROPNAME_BOUNDS = "android:changeBounds:bounds";
private static final String PROPNAME_CLIP = "android:changeBounds:clip";
private static final String PROPNAME_PARENT = "android:changeBounds:parent"; //父容器是否响应
private static final String PROPNAME_WINDOW_X = "android:changeBounds:windowX"; //屏幕位置
private static final String PROPNAME_WINDOW_Y = "android:changeBounds:windowY";
  • ChangeScroll : scrollX/scrollY 属性
  • ChangeTransform : AnimationMatrix 属性
  • ChangeClipBounds : clipBounds 属性
  • ChangeImageTransform :ImageView 的 animateTransform 属性

TransitionSet

装饰器 TransitionSet 代表着 Transition 集合,并能够为其指定播放顺序。默认实现 AutoTransition 采用顺序播放,包括三个 Transition,即 Fade.OUT,ChangeBounds,Fade.IN。

单个 Transition 可以添加或排除某个 View,排除的规则可以使用 id,transitionname字符串,class等手段。

二维路径插值器(PathMotion)

PathMotion 负责在起始点之间进行插值,它返回的是一个路径。

1
public abstract Path getPath(float startX, float startY, float endX, float endY);

某些改变位置的 Transition 可以使用,如ChangeBounds

1
new ChangeBounds().setPathMotion(new ArcMotion())

Transition 的实践

活动间的转场动画

使用 ActivityOptions可以得到活动间的转场动画,这里要通过 transitionName 属性来指定采用动画的View。

1
2
3
String transitionName = context.getResources().getString(R.string.transition_app_icon);
ActivityOptions transitionActivityOptions = ActivityOptions.makeSceneTransitionAnimation(activity, appIcon, transitionName);
context.startActivity(intent, transitionActivityOptions.toBundle());

1.执行这个动画需要设置 Window.FEATURE_ACTIVITY_TRANSITIONS。

ActivityOptions 创建的各种动画只是记录参数到 Bundle 中去,对于 SceneTransition 主要记录 View 和 String(transitionName) 的对应关系,以便于选取 Transition。

2.使用转场动画时,可以使用下列的一对方法来控制转场动画。

1
2
3
void postponeEnterTransition(); //使得动画阻塞
//Thread.sleep(3000); //可以利用这段时间做准备工作,进行数据的加载
void startPostponedEnterTransition(); //启动动画,否则动画将一直阻塞下去,新Activit无法启动

3.更简单的使用场景动画的方式是

1
2
startActivity(intent);
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right);

overridePendingTransition 分别决定了起始页和跳转页使用的属性动画。

这一点同样可以用在离开时

1
2
3
4
5
@Override
public void onBackPressed() {
super.onBackPressed();
overridePendingTransition(R.anim.slide_out_right, R.anim.slide_in_left);
}

4.使用 Transition 监听器

活动的 Transition 是设置在 Window 上的,采用转场动画应该尽量在动画播放完成后再加载数据,避免处理控件(如RecyclerView)的滑动等。

1
getWindow().getEnterTransition().addListener(this);

自定义Transition

自定义Transition的核心是要改变容器布局中哪些 View 的哪些属性,为此需要以下步骤来确定(以改变TextView的字体大小为例)

1.确定属性名称

1
public static final String PRO_NAME_TEXT_SIZE = "text:size";

2.如何获取起始值TransitionValues

1
2
3
4
5
6
7
@Override
public void captureStartValues(TransitionValues transitionValues) {
if(transitionValues.view instanceof TextView){
TextView tv = (TextView) transitionValues.view;
transitionValues.values.put(PRO_NAME_TEXT_SIZE, tv.getTextSize());
}
}

3.创建关于字体大小的动画

1
2
3
4
5
6
7
8
9
10
11
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
if(endValues.view instanceof TextView){
TextView tv = (TextView) endValues.view;
float start = ((TextView) startValues.view).getTextSize(); //初始值
float end = ((TextView) endValues.view).getTextSize(); //终止值
PropertyValuesHolder holder = PropertyValuesHolder.ofFloat(PROPERTY_TEXT_SIZE, start, end);
return ObjectAnimator.ofPropertyValuesHolder(tv, holder);
}
return null;
}

这样当设置字体时就会播放动画。

TransitionPlayer

TransitionPlayer 中有一个精彩的引导页示例, 也是 Transition 动画的使用范例。

Material-Animations

StarWars.Android

img