Drawable繪制過(guò)程源碼分析和自定義Drawable實(shí)現(xiàn)動(dòng)畫(huà)

1. 概述

對(duì)于Drawable掺喻,相信大家都不陌生,而且用起來(lái)非常方便颇玷。在Android中Drawable代表可以在canvas上被繪制的一般抽象,與View或者其子類(lèi)相比就缆,不需要進(jìn)行measure帖渠、layout操作,僅僅只需要進(jìn)行draw操作违崇。

除了簡(jiǎn)單的draw操作之外阿弃,Drawable還提供了許多通用的機(jī)制,以便與正在繪制的圖形進(jìn)行交互:
1> 為了告訴Drawable繪制的位置以及它的大小羞延,setBounds方法必須在繪制之前被調(diào)用。通称⒒梗可以通過(guò)getIntrinsicHeight和getIntrinsicWidth方法找到某些Drawable的首選大小伴箩,比如BitmapDrawable通過(guò)getIntrinsicHeight和getIntrinsicWidth方法獲取bitmap的高和寬。

2> getPadding方法可以從某些Drawables返回關(guān)于如何放置其內(nèi)容的信息鄙漏。 例如嗤谚,一個(gè)想要作為button widget的Drawable將需要返回將label正確放置在其內(nèi)部的padding;LayerDrawable通過(guò)子Drawable的getPadding方法獲取子Drawable的padding怔蚌,從而進(jìn)行padding的積累巩步,后面會(huì)講解LayerDrawable 中padding的積累導(dǎo)致的設(shè)計(jì)缺陷。

3> setState方法可以告知Drawable在哪個(gè)狀態(tài)下繪制桦踊,例如“focus”椅野,“selected”等。一些drawables可以根據(jù)所選狀態(tài)修改其圖像籍胯,比如StateListDrawable竟闪,關(guān)于StateListDrawable的使用可以參考Drawable Resources與Color State List Resource 中的StateListDrawable部分,如果想要知道StateListDrawable中是如何根據(jù)狀態(tài)繪制的杖狼,可以參考StateListDrawable 類(lèi)的onStateChange方法炼蛤。

4> setLevel方法允許提供單個(gè)連續(xù)控制器來(lái)修改Drawable的顯示,例如電池電量蝶涩。 一些drawables可以根據(jù)當(dāng)前l(fā)evel修改其圖像理朋,比如LevelListDrawable絮识,關(guān)于LevelListDrawable的使用可以參考可繪制對(duì)象資源,如果想要知道LevelListDrawable中是如何根據(jù)level繪制的嗽上,可以參考LevelListDrawable類(lèi)的onLevelChange方法次舌。

5> Drawable可以通過(guò)Drawable.Callback接口回調(diào)其client來(lái)執(zhí)行動(dòng)畫(huà)。 所有client都應(yīng)該支持此interface(通過(guò)setCallback)炸裆,以便動(dòng)畫(huà)可以正常工作垃它。 一個(gè)簡(jiǎn)單的方法是通過(guò)系統(tǒng)設(shè)施,如android.view.View#setBackground(Drawable)和 android.widget.ImageView烹看。在下面自定義Drawable的講解中會(huì)詳細(xì)介紹通過(guò)Drawable.Callback怎么實(shí)現(xiàn)動(dòng)畫(huà)国拇。

從API 24開(kāi)始,自定義的Drawable也可以在XML中使用惯殊,考慮到兼容性的問(wèn)題酱吝,該特性目前并沒(méi)有什么用,希望以后Google可以將該特性兼容到低版本土思。

2. 預(yù)備知識(shí)

2.1 Paint中setPathEffect(PathEffect effect)方法的使用

顧名思義务热,PathEffect是用來(lái)對(duì)Paint繪制的Path設(shè)置樣式的,PathEffect本身并沒(méi)有具體實(shí)現(xiàn)己儒,我們通常使用其子類(lèi):



接下來(lái)就來(lái)看看這六個(gè)子類(lèi)的具體效果:



上圖中第一條曲線是原始的Path崎岂,從第二條開(kāi)始依次使用了DashPathEffect、CornerPathEffect闪湾、DiscretePathEffect冲甘、PathDashPathEffect、ComposePathEffect和SumPathEffect途样。
實(shí)現(xiàn)代碼如下所示:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_test_path);
    pathView = findViewById(R.id.view_path);
    dashPathEffectView = findViewById(R.id.view_dash_path_effect);
    cornerPathEffectView = findViewById(R.id.view_corner_path_effect);
    discretePathEffectView = findViewById(R.id.view_discrete_path_effect);
    pathDashPathEffectView = findViewById(R.id.view_path_dash_path_effect);
    composePathEffectView = findViewById(R.id.view_compose_path_effect);
    sumPathEffectView = findViewById(R.id.view_sum_path_effect);

    Path path = new Path();
    path.moveTo(50, 30);
    path.lineTo(200, 230);
    path.lineTo(400, 30);
    path.lineTo(600, 230);
    path.lineTo(800, 30);
    path.lineTo(1000, 230);

    float dashGap = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics());
    float dashWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics());
    DashPathEffect dashPathEffect = new DashPathEffect(new float[]{dashGap, dashWidth}, 0);
    CornerPathEffect cornerPathEffect = new CornerPathEffect(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics()));
    DiscretePathEffect discretePathEffect = new DiscretePathEffect(3, 5);
    PathDashPathEffect pathDashPathEffect = new PathDashPathEffect(
            makeConvexArrow(24.0f, 14.0f),    // "stamp"
            36.0f,                            // advance, or distance between two stamps
            0.0f,                             // phase, or offset before the first stamp
            PathDashPathEffect.Style.ROTATE); // how to transform each stamp
    ComposePathEffect composePathEffect = new ComposePathEffect(dashPathEffect, cornerPathEffect);
    SumPathEffect sumPathEffect = new SumPathEffect(cornerPathEffect, dashPathEffect);

    TrackShape trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()));
    TrackShapeDrawable trackShapeDrawable = new TrackShapeDrawable(trackShape);
    pathView.setBackground(trackShapeDrawable);
    trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()), dashPathEffect);
    trackShapeDrawable = new TrackShapeDrawable(trackShape);
    dashPathEffectView.setBackground(trackShapeDrawable);
    trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()), cornerPathEffect);
    trackShapeDrawable = new TrackShapeDrawable(trackShape);
    cornerPathEffectView.setBackground(trackShapeDrawable);
    trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()), discretePathEffect);
    trackShapeDrawable = new TrackShapeDrawable(trackShape);
    discretePathEffectView.setBackground(trackShapeDrawable);
    trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()), pathDashPathEffect);
    trackShapeDrawable = new TrackShapeDrawable(trackShape);
    pathDashPathEffectView.setBackground(trackShapeDrawable);
    trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()), composePathEffect);
    trackShapeDrawable = new TrackShapeDrawable(trackShape);
    composePathEffectView.setBackground(trackShapeDrawable);
    trackShape = new TrackShape(path, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()), sumPathEffect);
    trackShapeDrawable = new TrackShapeDrawable(trackShape);
    sumPathEffectView.setBackground(trackShapeDrawable);
}

private Path makeConvexArrow(float length, float height) {
    Path p = new Path();
    p.moveTo(0.0f, -height / 2.0f);
    p.lineTo(length - height / 4.0f, -height / 2.0f);
    p.lineTo(length, 0.0f);
    p.lineTo(length - height / 4.0f, height / 2.0f);
    p.lineTo(0.0f, height / 2.0f);
    p.lineTo(0.0f + height / 4.0f, 0.0f);
    p.close();
    return p;
}

上面代碼中的TrackShapeDrawable和TrackShape在下面的自定義Drawable中會(huì)詳細(xì)講解江醇,最終PathEffect會(huì)通過(guò)paint.setPathEffect(pathEffect);方式進(jìn)行應(yīng)用。
下面我們來(lái)分析一下PathEffect六個(gè)子類(lèi)的使用方法:

DashPathEffect -- 用來(lái)實(shí)現(xiàn)Path的虛線效果
構(gòu)造方法簽名如下:
public DashPathEffect(float intervals[], float phase) 
虛線是由一系列的線段組成何暇,intervals數(shù)組長(zhǎng)度必須是大于等于2的偶數(shù)陶夜,偶數(shù)索引指定線段長(zhǎng)度,而奇數(shù)索引指定線段之間間隔長(zhǎng)度裆站。
例如 PathEffect effects = new DashPathEffect(new float[] { 1, 2, 4, 8}, 1);表示
繪制長(zhǎng)度1的線段,再繪制長(zhǎng)度2的空白,再繪制長(zhǎng)度4的線段,再繪制長(zhǎng)度8的空白,依次重復(fù)条辟。
phase參數(shù)表示起始位置的偏移量(通過(guò)這個(gè)值可以實(shí)現(xiàn)虛線滾動(dòng)的動(dòng)畫(huà))

CornerPathEffect -- 用來(lái)實(shí)現(xiàn)Path拐角處的圓角效果
構(gòu)造方法簽名如下:
public CornerPathEffect(float radius)
1. radius 圓角的半徑

DiscretePathEffect -- 用來(lái)實(shí)現(xiàn)Path的離散效果
構(gòu)造方法簽名如下:
public DiscretePathEffect(float segmentLength, float deviation)
將Path分割成長(zhǎng)度為segmentLength的片段,然后在原始Path的基礎(chǔ)上發(fā)生deviation大小的隨機(jī)偏離

PathDashPathEffect -- 用來(lái)實(shí)現(xiàn)Path的虛線效果(虛線中每一個(gè)線段都用Path對(duì)應(yīng)的圖形代替)
構(gòu)造方法簽名如下:
public PathDashPathEffect(Path shape, float advance, float phase, Style style)
1. shape參數(shù):虛線中每一個(gè)線段對(duì)應(yīng)的Path
2. advance參數(shù):每一個(gè)shape之間的間距
3. phase參數(shù):起始位置的偏移量(通過(guò)這個(gè)值可以實(shí)現(xiàn)虛線滾動(dòng)的動(dòng)畫(huà))
4. style參數(shù):代表每一個(gè)shape之間是如何銜接的遏插,一共有如下三種類(lèi)型:
             TRANSLATE  平移的方式
             ROTATE         旋轉(zhuǎn)的方式
             MORPH         變形的方式
   上面三種方式的具體效果捂贿,大家有興趣可以嘗試一下

ComposePathEffect、SumPathEffect -- 用來(lái)將上面提到的PathEffect進(jìn)行組合胳嘲,然后作用于Path上
它們之間不同在于組合的方式厂僧,ComposePathEffect(PathEffect outerpe, PathEffect innerpe)會(huì)先將
innerpe的效果應(yīng)用到Path上,然后再將outerpe的效果應(yīng)用到 應(yīng)用了innerpe效果的Path 上了牛,
即outer(inner(path))颜屠;而SumPathEffect(PathEffect first, PathEffect second)則會(huì)依次應(yīng)用
兩個(gè)效果到Path上辰妙,即 first(path) + second(path)。

2.2 Paint中setShader(Shader shader)方法的使用

顧名思義甫窟,Paint繪制任何對(duì)象(除了bitmap)時(shí)都是通過(guò)Shader得到color值密浑,Shader本身并沒(méi)有具體實(shí)現(xiàn),我們通常使用其子類(lèi):



在講解這五個(gè)子類(lèi)之前粗井,必須先了解一下Shader和Shader中的TileMode:

Shader:前面說(shuō)過(guò)Paint繪制任何對(duì)象(除了bitmap)時(shí)都是通過(guò)Shader得到color值尔破,
Shader獲取指定坐標(biāo)的color值是基于View布局區(qū)域(屏幕區(qū)域中排出狀態(tài)欄區(qū)域和應(yīng)用標(biāo)題欄區(qū)域后
剩余的區(qū)域)對(duì)應(yīng)的坐標(biāo)系。

Shader的原始區(qū)域:也就是Shader子類(lèi)(除了BitmapShader)的漸變區(qū)域浇衬,
BitmapShader的原始區(qū)域就是第一張bitmap繪制的區(qū)域懒构。

CLAMP : 如果繪制區(qū)域超出Shader的原始區(qū)域,則通過(guò)復(fù)制邊緣顏色來(lái)填充超過(guò)的區(qū)域耘擂。

REPEAT : 如果繪制區(qū)域超出Shader的原始區(qū)域胆剧,則通過(guò)在水平和垂直方向重復(fù)Shader的原始區(qū)域
來(lái)填充超過(guò)的區(qū)域。

MIRROR : 和REPEAT類(lèi)似醉冤,不過(guò)是以鏡像的方式重復(fù)秩霍。

注意:
1 BitmapShader可以在水平和垂直方向上同時(shí)設(shè)置TileMode,那么Shader會(huì)先在垂直方向上進(jìn)行
TileMode對(duì)應(yīng)的操作蚁阳,然后再在水平方向上進(jìn)行TileMode對(duì)應(yīng)的操作铃绒。
2 繪制區(qū)域是不可能超過(guò)SweepGradient的原始區(qū)域。
3 LinearGradient和RadialGradient只在一個(gè)方向上進(jìn)行漸變螺捐,因此應(yīng)用一個(gè)TileMode就足夠了匿垄。
4 在創(chuàng)建Shader子類(lèi)的實(shí)例時(shí),TileMode不可以為null归粉,否則會(huì)報(bào)NullPointerException異常。

原理講了這么多漏峰,接下來(lái)我們就通過(guò)例子來(lái)加以驗(yàn)證:

1. BitmapShader

水平和垂直方向上的TileMode都是CLAMP時(shí):



上圖中左上角的圖片就是BitmapShader的原始區(qū)域糠悼。

實(shí)現(xiàn)代碼如下

public class BitmapShaderView extends View {

    private Paint mPaint;

    public BitmapShaderView(Context context) {
        super(context);
    }

    public BitmapShaderView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BitmapShaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bitmap_shader);
        mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(0, 0, 1000, 1800, mPaint);
    }
}

結(jié)合運(yùn)行截圖和代碼可以證明:
對(duì)于CLAMP,如果繪制區(qū)域超出Shader的原始區(qū)域浅乔,則通過(guò)復(fù)制邊緣顏色來(lái)填充超過(guò)的區(qū)域倔喂。

水平和垂直方向上的TileMode分別是CLAMP,MIRROR:



上圖中左上角的圖片就是BitmapShader的原始區(qū)域靖苇。

實(shí)現(xiàn)代碼和上面基本相同席噩,不同的地方如下:

mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.MIRROR));

結(jié)合運(yùn)行截圖和代碼可以證明:
1> 對(duì)于MIRROR,如果繪制區(qū)域超出Shader的原始區(qū)域贤壁,則通過(guò)在水平和垂直方向以鏡像的方式重復(fù)Shader的原始區(qū)域來(lái)填充超過(guò)的區(qū)域悼枢。
2> BitmapShader可以在水平和垂直方向上同時(shí)設(shè)置TileMode,那么Shader會(huì)先在垂直方向上進(jìn)行TileMode對(duì)應(yīng)的操作脾拆,然后再在水平方向上進(jìn)行TileMode對(duì)應(yīng)的操作

水平和垂直方向上的TileMode都是REPEAT:



上圖中左上角的圖片就是BitmapShader的原始區(qū)域馒索。

實(shí)現(xiàn)代碼和上面基本相同莹妒,不同的地方如下:

mPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));

結(jié)合運(yùn)行截圖和代碼可以證明:
對(duì)于REPEAT,如果繪制區(qū)域超出Shader的原始區(qū)域绰上,則通過(guò)在水平和垂直方向重復(fù)Shader的原始區(qū)域來(lái)填充超過(guò)的區(qū)域旨怠。

2. LinearGradient 線性漸變

上圖第二條傾斜的漸變區(qū)域就是LinearGradient的原始區(qū)域。
實(shí)現(xiàn)代碼與BitmapShader中的基本相同蜈块,不同的地方如下:

mPaint.setShader(new LinearGradient(200, 200, 400, 400, new int[]{Color.RED, Color.YELLOW}, null, Shader.TileMode.REPEAT));

代碼中LinearGradient的前四個(gè)參數(shù)決定了LinearGradient的原始區(qū)域?yàn)樯蠄D第二條傾斜的漸變區(qū)域鉴腻,第五個(gè)參數(shù)代表漸變的顏色值,關(guān)于最后連個(gè)參數(shù)百揭,有興趣的同學(xué)可以自己研究一下爽哎。

3. RadialGradient 徑向漸變

上圖最小的圓區(qū)域就是RadialGradient的原始區(qū)域。
實(shí)現(xiàn)代碼和上面基本相同信峻,不同的地方如下:

mPaint.setShader(new RadialGradient(400, 400, 100, new int[]{Color.RED, Color.YELLOW}, null, Shader.TileMode.REPEAT));

代碼中RadialGradient的前三個(gè)參數(shù)決定了RadialGradient的原始區(qū)域?yàn)樯蠄D最小的圓區(qū)域倦青,第四個(gè)參數(shù)代表漸變的顏色值,關(guān)于最后連個(gè)參數(shù)盹舞,有興趣的同學(xué)可以自己研究一下产镐。

4. SweepGradient 梯度漸變

實(shí)現(xiàn)代碼和上面基本相同,不同的地方如下:

mPaint.setShader(new SweepGradient(400, 400, new int[]{Color.RED, Color.YELLOW}, null));

代碼中SweepGradient的前兩個(gè)參數(shù)決定了SweepGradient的原始區(qū)域?yàn)闊o(wú)限大區(qū)域(從而證明了 繪制區(qū)域是不可能超過(guò)SweepGradient的原始區(qū)域)踢步,第四個(gè)參數(shù)代表漸變的顏色值癣亚,關(guān)于最后一個(gè)參數(shù),有興趣的同學(xué)可以自己研究一下获印。

5. ComposeShader

顧名思義述雾,ComposeShader是用來(lái)組合使用上面的任意兩種Shader。



實(shí)現(xiàn)代碼和上面基本相同兼丰,不同的地方如下:

mPaint.setShader(new ComposeShader(new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT),
        new LinearGradient(200, 200, 400, 400, new int[]{Color.RED, Color.TRANSPARENT}, null, Shader.TileMode.REPEAT), PorterDuff.Mode.SRC_OVER));

代碼中ComposeShader的前兩個(gè)參數(shù)代表兩個(gè)被組合的Shader玻孟,第三個(gè)參數(shù)代表組合的方式,首先我們通過(guò)下圖直觀的看一下所有組合的方式:



代碼中ComposeShader的第一個(gè)參數(shù)對(duì)應(yīng)上圖中的Des鳍征,第二個(gè)參數(shù)對(duì)應(yīng)上圖中的Src黍翎,講到這里相信大家就應(yīng)該可以看的懂上面的代碼了。

講到這里艳丛,大家有沒(méi)有這樣的疑問(wèn):怎么對(duì)通過(guò)Shader繪制的圖形進(jìn)行縮放匣掸、平移、旋轉(zhuǎn)和錯(cuò)切操作呢氮双?那么這是就要用到Shader的setLocalMatrix方法了碰酝,使用方式和下面2.3中講到的用法一樣,這里就不再贅敘了戴差。

2.3 Path中transform方法的使用

Path中的transform方法是實(shí)現(xiàn)Path的scale送爸、rotate、translate和skew動(dòng)畫(huà)的關(guān)鍵,下面舉個(gè)例子來(lái)說(shuō)明一下transform方法是如何使用的碱璃,首先看一下運(yùn)行截圖:



實(shí)現(xiàn)代碼如下:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_test_path_transform);
    // 用來(lái)顯示坐標(biāo)軸的
    coordinateView = findViewById(R.id.view_coordinate);
    // 用來(lái)顯示左上角的矩形
    testPathTransformView = findViewById(R.id.view_test_path);
    // 用來(lái)顯示右上角的矩形
    testPathScaleView = findViewById(R.id.view_test_path_scale);
    // 用來(lái)顯示左下角的矩形
    testPathRotateView = findViewById(R.id.view_test_path_rotate);
    // 用來(lái)顯示右下角的矩形
    testPathSkewView = findViewById(R.id.view_test_path_skew);

    // 構(gòu)建坐標(biāo)軸對(duì)應(yīng)的Path
    Path coordinate = new Path();
    coordinate.moveTo(0, 100);
    coordinate.lineTo(1000, 100);
    coordinate.lineTo(980, 80);
    coordinate.moveTo(1000, 100);
    coordinate.lineTo(980, 120);
    coordinate.moveTo(100, 0);
    coordinate.lineTo(100, 1000);
    coordinate.lineTo(80, 980);
    coordinate.moveTo(100, 1000);
    coordinate.lineTo(120, 980);
    TrackShape coordinateShape = new TrackShape(coordinate, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()));
    TrackShapeDrawable coordinateShapeDrawable = new TrackShapeDrawable(coordinateShape);
    coordinateView.setBackground(coordinateShapeDrawable);

    // 構(gòu)建左上角矩形對(duì)應(yīng)的Path
    Path rectPath = new Path();
    rectPath.addRect(new RectF(200, 200, 400, 400), Path.Direction.CW);
    TrackShape rectTrackShape = new TrackShape(rectPath, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()));
    TrackShapeDrawable rectShapeDrawable = new TrackShapeDrawable(rectTrackShape);
    rectShapeDrawable.getPaint().setShader(new SweepGradient(300, 300, new int[]{Color.WHITE, 0xFF00C7B2}, null));
    testPathTransformView.setBackground(rectShapeDrawable);

    Matrix translateMatrix1 = new Matrix();
    translateMatrix1.setTranslate(400, 0);
    Path scaleRectPath = new Path();
    // 將左上角矩形對(duì)應(yīng)的Path向右平移400個(gè)像素
    rectPath.transform(translateMatrix1, scaleRectPath);
    Matrix scaleMatrix = new Matrix();
    scaleMatrix.setScale(0.5f, 0.5f, 700, 300);
    // 將平移后得到的Path相對(duì)于矩形的中心點(diǎn)縮小一半
    scaleRectPath.transform(scaleMatrix);
    TrackShape scaleRectTrackShape = new TrackShape(scaleRectPath, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()));
    TrackShapeDrawable scaleRectShapeDrawable = new TrackShapeDrawable(scaleRectTrackShape);
    scaleRectShapeDrawable.getPaint().setShader(new SweepGradient(700, 300, new int[]{Color.WHITE, 0xFF00C7B2}, null));
    testPathScaleView.setBackground(scaleRectShapeDrawable);

    Matrix translateMatrix2 = new Matrix();
    translateMatrix2.setTranslate(0, 400);
    Path rotateRectPath = new Path();
    // 將左上角矩形對(duì)應(yīng)的Path向下平移400個(gè)像素
    rectPath.transform(translateMatrix2, rotateRectPath);
    Matrix rotateMatrix = new Matrix();
    rotateMatrix.setRotate(60, 300, 700);
    // 將平移后得到的Path相對(duì)于矩形的中心點(diǎn)旋轉(zhuǎn)180度
    rotateRectPath.transform(rotateMatrix);
    TrackShape rotateRectTrackShape = new TrackShape(rotateRectPath, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()));
    TrackShapeDrawable rotateRectShapeDrawable = new TrackShapeDrawable(rotateRectTrackShape);
    rotateRectShapeDrawable.getPaint().setShader(new SweepGradient(300, 700, new int[]{Color.WHITE, 0xFF00C7B2}, null));
    testPathRotateView.setBackground(rotateRectShapeDrawable);

    Matrix translateMatrix3 = new Matrix();
    translateMatrix3.setTranslate(400, 400);
    Path skewRectPath = new Path();
    // 將左上角矩形對(duì)應(yīng)的Path向上向下分別平移400個(gè)像素
    rectPath.transform(translateMatrix3, skewRectPath);
    Matrix skewMatrix = new Matrix();
    skewMatrix.setSkew(-0.5f, 0, 700, 700);
    // 將平移后得到的Path相對(duì)于矩形的中心點(diǎn)在水平方向上錯(cuò)切矩形寬度的一半
    skewRectPath.transform(skewMatrix);
    TrackShape skewRectTrackShape = new TrackShape(skewRectPath, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()));
    TrackShapeDrawable skewRectShapeDrawable = new TrackShapeDrawable(skewRectTrackShape);
    skewRectShapeDrawable.getPaint().setShader(new SweepGradient(700, 700, new int[]{Color.WHITE, 0xFF00C7B2}, null));
    testPathSkewView.setBackground(skewRectShapeDrawable);
}

activity_test_path_transform.xml代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/view_coordinate"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <View
        android:id="@+id/view_test_path"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <View
        android:id="@+id/view_test_path_scale"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <View
        android:id="@+id/view_test_path_rotate"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <View
        android:id="@+id/view_test_path_skew"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

上面的注釋非常清晰了弄痹,這里就不再贅敘了。上面代碼中的TrackShapeDrawable和TrackShape在下面的自定義Drawable中會(huì)詳細(xì)講解嵌器。

3. Drawable繪制過(guò)程源碼分析

前面已經(jīng)講到過(guò)肛真,Drawable與View或者其子類(lèi)相比,不需要進(jìn)行measure爽航、layout操作蚓让,僅僅只需要進(jìn)行draw操作,因此我們只要明白Drawable的繪制過(guò)程讥珍,基本上就可以實(shí)現(xiàn)自定義Drawable历极。Drawable是用來(lái)在View上顯示的,因此View的繪制過(guò)程中就會(huì)繪制View上要顯示的Drawable(即調(diào)用Drawable的onDraw方法)衷佃。在前面的一篇博客Android View的測(cè)量趟卸、布局、繪制流程源碼分析及自定義View實(shí)例演示中講解了View的測(cè)量氏义、布局锄列、繪制流程,希望有興趣的同學(xué)可以看看惯悠,下面我就直接從View的drawBackground方法(該方法繪制View的繪制過(guò)程中被調(diào)用)入手研究Drawable的繪制過(guò)程邻邮,顧名思義,drawBackground方法是用來(lái)繪制View的背景的克婶,首先我們先看一下繪制View的背景時(shí)序圖:

View背景繪制時(shí)序圖

上圖表明繪制View的背景的任務(wù)主要分為兩部分:
1> 設(shè)置View的背景對(duì)應(yīng)的Drawable的大小筒严,即設(shè)置Drawable繪制區(qū)域的大小 (上圖中的1、2情萤、3鸭蛙、4步)
2> 在畫(huà)布上繪制View的背景對(duì)應(yīng)的Drawable (上圖中的5步)
首先看一下上圖中第1步的drawBackground方法的源碼:

private void drawBackground(Canvas canvas) {
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }
    // 設(shè)置View的背景對(duì)應(yīng)的Drawable的大小,即設(shè)置Drawable繪制區(qū)域的大小
    setBackgroundBounds();

    // Attempt to use a display list if requested.
    if (canvas.isHardwareAccelerated() && mAttachInfo != null
            && mAttachInfo.mHardwareRenderer != null) {
        mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);

        final RenderNode renderNode = mBackgroundRenderNode;
        if (renderNode != null && renderNode.isValid()) {
            setBackgroundRenderNodeProperties(renderNode);
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
            return;
        }
    }

    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
     //當(dāng)沒(méi)有發(fā)生滑動(dòng)時(shí),直接進(jìn)行繪制筋岛,如果發(fā)生了滑動(dòng)偏移规惰,就先將畫(huà)布的坐標(biāo)系進(jìn)行對(duì)應(yīng)的偏移。
    if ((scrollX | scrollY) == 0) {
        background.draw(canvas);
    } else {
        canvas.translate(scrollX, scrollY);
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}

上面的注釋很清楚泉蝌,就不做贅敘了。
接著我們來(lái)看上圖中第2步中setBackgroundBounds方法的源碼:

void setBackgroundBounds() {
    if (mBackgroundSizeChanged && mBackground != null) {
        // 將View的背景對(duì)應(yīng)的Drawable的大小設(shè)置為View的大小揩晴,即設(shè)置Drawable繪制區(qū)域的大小 
        mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
        mBackgroundSizeChanged = false;
        rebuildOutline();
    }
}

上面的注釋很清楚勋陪,就不做贅敘了。
接著我們來(lái)看看第3步中的源碼:

/**
 * Specify a bounding rectangle for the Drawable. This is where the drawable
 * will draw when its draw() method is called.
 */
public void setBounds(int left, int top, int right, int bottom) {
    Rect oldBounds = mBounds;

    if (oldBounds == ZERO_BOUNDS_RECT) {
        oldBounds = mBounds = new Rect();
    }

    if (oldBounds.left != left || oldBounds.top != top ||
            oldBounds.right != right || oldBounds.bottom != bottom) {
        if (!oldBounds.isEmpty()) {
            // first invalidate the previous bounds
            invalidateSelf();
        }
        mBounds.set(left, top, right, bottom);
        // 在自定義Drawable時(shí)就是通過(guò)重載這個(gè)方法獲取到自定義Drawable的繪制區(qū)域
        onBoundsChange(mBounds);
    }
}

上面的注釋很清楚硫兰,就不做贅敘了诅愚。
回到第1步的drawBackground方法中,設(shè)置完View的背景對(duì)應(yīng)的Drawable的繪制區(qū)域后(即setBackgroundBounds方法調(diào)用結(jié)束后),接著就是在繪制區(qū)域上繪制圖像(即上圖中的第5步违孝,Drawable的draw方法會(huì)被調(diào)用)刹前,Drawable的draw方法是一個(gè)抽象方法,因此自定義Drawable時(shí)必須實(shí)現(xiàn)該方法:

public abstract void draw(@NonNull Canvas canvas);

4. 自定義Drawable

首先讓大家看看我通過(guò)自定義Drawable實(shí)現(xiàn)的一個(gè)動(dòng)畫(huà):



大多數(shù)開(kāi)發(fā)者者通常第一個(gè)會(huì)想到通過(guò)自定義View來(lái)實(shí)現(xiàn)上面的動(dòng)畫(huà)效果雌桑,雖然可以實(shí)現(xiàn)上面的動(dòng)畫(huà)喇喉,但是通用性不是很好,而且實(shí)現(xiàn)的復(fù)雜度也很高校坑;通過(guò)自定義Drawable的方式實(shí)現(xiàn)上面的動(dòng)畫(huà)效果的好處如下:

  • 通用性好:實(shí)現(xiàn)的動(dòng)畫(huà)效果可以適用于View及其子類(lèi)
  • 實(shí)現(xiàn)起來(lái)很簡(jiǎn)單:與自定義View相比拣技,不需要進(jìn)行measure、layout操作耍目,僅僅只需要進(jìn)行draw操作

1> 分析上面的動(dòng)畫(huà)

  • 動(dòng)畫(huà)的初始狀態(tài):是兩個(gè)圖層疊加的想過(guò)膏斤,底層是一個(gè)50%透明度的白色圓形圖層(后面統(tǒng)一稱(chēng)為A層)茄菊,上層是一個(gè)居中顯示鬧鐘圖片的圖層(后面統(tǒng)一稱(chēng)為B層)朝巫。
  • 動(dòng)畫(huà)的中間過(guò)程:A層的透明度變成50%,然后B層中鬧鐘圖片上方顯示了一個(gè)與鬧鐘圖片大小相同的透明度為20%的白色圓形圖層(后面統(tǒng)一稱(chēng)為C層)结啼,然后在C層上面動(dòng)態(tài)的放大一個(gè)與C層上圓形半徑相同的灰色的圓環(huán)(后面統(tǒng)一稱(chēng)為D層)毅访,然后在然后在D層上面動(dòng)態(tài)的繪制一個(gè)與D層上圓形半徑相同的綠色圓環(huán)(后面統(tǒng)一稱(chēng)為E層)沮榜,接著在E層的中心位置動(dòng)態(tài)繪制一個(gè)從小變大的綠色對(duì)勾(后面統(tǒng)一稱(chēng)作F層),接著就是顯示動(dòng)畫(huà)的結(jié)束狀態(tài)俺抽。
  • 動(dòng)畫(huà)的結(jié)束狀態(tài):A層的透明度變成0敞映,B層與初始狀態(tài)一樣, C磷斧、D振愿、E、F層不在顯示弛饭。

通過(guò)上面的分析可知冕末,上面的動(dòng)畫(huà)效果要通過(guò)6個(gè)圖層實(shí)現(xiàn),因此可以通過(guò)6個(gè)Drawable的疊加來(lái)實(shí)現(xiàn)侣颂,A档桃、B、C層由于沒(méi)有動(dòng)畫(huà)效果(A層只不過(guò)是在動(dòng)畫(huà)開(kāi)始和結(jié)束時(shí)改變了透明度)憔晒,因此實(shí)現(xiàn)起來(lái)很簡(jiǎn)單藻肄。

2> 實(shí)現(xiàn)A、B拒担、C層

實(shí)現(xiàn)代碼如下所示:

/*
* A層
*/
float radius = DisplayUtils.dip2px(getContext(), 38f);
GradientDrawable bigBgCircle = new GradientDrawable();
float[] radii = new float[]{radius, radius, radius, radius,
        radius, radius, radius, radius};
bigBgCircle.setColor(0xAAFFFFFF);
bigBgCircle.setCornerRadii(radii);

/*
* B層
*/
int padding = DisplayUtils.dip2px(getContext(), 19f);
InsetDrawable clock = new InsetDrawable(getResources().getDrawable(R.drawable.ic_clock), padding);

/*
* C層
*/
float radius2 = DisplayUtils.dip2px(getContext(), 19f);
GradientDrawable smallBgCircleCenter = new GradientDrawable();
radii = new float[]{radius2, radius2, radius2, radius2,
        radius2, radius2, radius2, radius2};
smallBgCircleCenter.setColor(0xDDFFFFFF);
smallBgCircleCenter.setCornerRadii(radii);
int padding2 = DisplayUtils.dip2px(getContext(), 19f);
InsetDrawable smallBgCircle = new InsetDrawable(smallBgCircleCenter, padding2);

LayerDrawable layerDrawable = new LayerDrawable(new Drawable[]{bigBgCircle, clock, smallBgCircle});
animatorDrawableView.setBackground(layerDrawable);

運(yùn)行截圖如下:



怎么只有A層和B層嘹屯,而沒(méi)有C層,其實(shí)這是LayerDrawable的設(shè)計(jì)缺陷从撼,padding在LayerDrawable中是累積的州弟,也就是說(shuō),在某一層的padding影響所有較高層的bounds,可以通過(guò)LayerDrawable的onBoundsChange方法的源碼看出來(lái):

@Override
protected void onBoundsChange(Rect bounds) {
    final ChildDrawable[] array = mLayerState.mChildren;
    final int N = mLayerState.mNum;
    int padL=0, padT=0, padR=0, padB=0;
    for (int i=0; i<N; i++) {
        final ChildDrawable r = array[i];
        r.mDrawable.setBounds(bounds.left + r.mInsetL + padL,
                              bounds.top + r.mInsetT + padT,
                              bounds.right - r.mInsetR - padR,
                              bounds.bottom - r.mInsetB - padB);
        padL += mPaddingL[i];
        padR += mPaddingR[i];
        padT += mPaddingT[i];
        padB += mPaddingB[i];
    }
}

因?yàn)镮nsetDrawable用inset作為padding婆翔,因此導(dǎo)致在B層上面的C層中的圓的半徑被縮小了19dp拯杠,因此C層中的圓就不見(jiàn)了。
在API 21的時(shí)候啃奴,Google意識(shí)到了這個(gè)設(shè)計(jì)缺陷潭陪,因此在LayerDrawable中添加了setPaddingMode方法,通過(guò)此方法可以設(shè)置LayerDrawable的PaddingMode為如下兩種:

/**
 * 默認(rèn)的PaddingMode纺腊,將每一層嵌入上一層的padding內(nèi)畔咧。
 *
 * @see #setPaddingMode(int)
 */
public static final int PADDING_MODE_NEST = 0;

/**
 * 將每一層直接堆疊在上一層之上。
 *
 * @see #setPaddingMode(int)
 */
public static final int PADDING_MODE_STACK = 1;

上面的注釋非常清楚揖膜,將LayerDrawable的PaddingMode的PaddingMode設(shè)置為PADDING_MODE_STACK就好了誓沸;雖然這種方法可以解決問(wèn)題,但是遺憾的是壹粟,為了兼容API 21以下的版本拜隧,這個(gè)方法是不可行的,因此為了避免這個(gè)問(wèn)題趁仙,我自定義了一個(gè)PaddingDrawable來(lái)取代InsetDrawable洪添,源碼如下:

public class PaddingDrawable extends Drawable {
    /**
     * 對(duì)應(yīng)要被設(shè)置padding的drawable
     */
    private Drawable drawable = null;
    private Paint paint = null;
    private Rect padding = null;

    public PaddingDrawable(Drawable drawable) {
        this.paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        this.drawable = drawable;
    }

    public Paint getPaint() {
        return paint;
    }

    public void setPadding(Rect padding) {
        if (padding == null) {
            return;
        }
        if (this.padding == null) {
            this.padding = new Rect();
        }
        this.padding.set(padding);
        invalidateSelf();
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        super.onBoundsChange(bounds);
        if (null == drawable) {
            return;
        }
        if (null == padding) {
            drawable.setBounds(bounds);
        } else {
            drawable.setBounds(bounds.left, bounds.top,
                    bounds.right - padding.left - padding.right, bounds.bottom - padding.top - padding.bottom);
        }
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        canvas.drawColor(Color.TRANSPARENT);
        drawDrawable(canvas);
    }

    private void drawDrawable(@NonNull Canvas canvas) {
        if (null == drawable) {
            return;
        }
        if (null != padding) {
            canvas.save();
            canvas.translate(padding.left, padding.top);
            drawable.draw(canvas);
            canvas.restore();
        } else {
            drawable.draw(canvas);
        }
    }

    @Override
    public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
        this.paint.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {
        this.paint.setColorFilter(colorFilter);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }
}

PaddingDrawable是通過(guò)自定義Drawable實(shí)現(xiàn)的,下面會(huì)詳細(xì)介紹自定義Drawable的雀费,因此大家只關(guān)注onBoundsChange和draw方法的實(shí)現(xiàn)就行了干奢,在2中Drawable繪制過(guò)程源碼分析已經(jīng)詳細(xì)介紹了onBoundsChange和draw方法的作用,相信大家應(yīng)該可以理解上面代碼的實(shí)現(xiàn)盏袄;接下來(lái)就是對(duì)上圖的實(shí)現(xiàn)代碼中的InsetDrawable替換成PaddingDrawable忿峻,代碼如下:

/*
* A層和上面相同,這里不再重復(fù)辕羽。
*/

/*
* B層
*/
PaddingDrawable clock = new PaddingDrawable(getResources().getDrawable(R.drawable.ic_clock));
int padding = DisplayUtils.dip2px(getContext(), 19f);
clock.setPadding(new Rect(padding, padding, padding, padding));

/*
* C層
*/
float radius2 = DisplayUtils.dip2px(getContext(), 19f);
GradientDrawable smallBgCircleCenter = new GradientDrawable();
radii = new float[]{radius2, radius2, radius2, radius2,
        radius2, radius2, radius2, radius2};
smallBgCircleCenter.setColor(0xDDFFFFFF);
smallBgCircleCenter.setCornerRadii(radii);
PaddingDrawable smallBgCircle = new PaddingDrawable(smallBgCircleCenter);
int padding2 = DisplayUtils.dip2px(getContext(), 19f);
smallBgCircle.setPadding(new Rect(padding2, padding2, padding2, padding2));

LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] {bigBgCircle, clock, smallBgCircle});
animatorDrawableView.setBackground(layerDrawable);

運(yùn)行截圖如下:



C層出現(xiàn)了逛尚,是我們想要的效果。

3> 實(shí)現(xiàn)D刁愿、E绰寞、F層

實(shí)現(xiàn)有動(dòng)畫(huà)效果的D、E铣口、F層就要用到自定義Drawable了滤钱,首先我們應(yīng)該繪制與D、E脑题、F層對(duì)應(yīng)的圓環(huán)和對(duì)勾菩暗,接著就是通過(guò)屬性動(dòng)畫(huà)讓D掏熬、E、F層中的圓環(huán)和對(duì)勾動(dòng)起來(lái)幔嫂。

繪制與D、E片吊、F層對(duì)應(yīng)的圓環(huán)和對(duì)勾

對(duì)于繪制圓環(huán)和對(duì)勾我借鑒了ShapeDrawable做法爷贫,那就是繼承Shape類(lèi)實(shí)現(xiàn)對(duì)于圓環(huán)和對(duì)勾的繪制,代碼如下:

/**
 * TrackShape 基本的軌跡圖形類(lèi)
 * 通過(guò)軌跡(即Path)來(lái)定義Shape,因此任何Shape的繪制過(guò)程就是繪制一條Path橱赠,
 * 即draw方法是一樣的并且在基類(lèi)TrackShape中實(shí)現(xiàn)宰啦。
 */
public class TrackShape extends Shape {

    protected Path track = null;

    /**
     * 代表縮放或者選擇后的track
     */
    private Path transformTrack = null;

    /**
     * 軌跡的厚度
     */
    protected float thickness = 0;

    /**
     * 設(shè)置軌跡為虛線
     */
    private PathEffect pathEffect = null;

    public TrackShape(Path track, float thickness) {
        this(track, thickness, null);
    }

    public TrackShape(Path track, float thickness, PathEffect pathEffect) {
        this.track = track;
        this.thickness = thickness;
        this.pathEffect = pathEffect;
    }

    public void scale(float widthScale, float heightScale) {
        scale(widthScale, heightScale, getWidth() / 2, getHeight() / 2);
    }

    /**
     * 當(dāng)發(fā)生縮放時(shí)被調(diào)用
     *
     * @param widthScale  橫向縮放的比例
     * @param heightScale 縱向縮放的比例
     * @param px          縮放中心點(diǎn)的x軸坐標(biāo)
     * @param py          縮放中心點(diǎn)的y軸坐標(biāo)
     */
    public void scale(float widthScale, float heightScale, float px, float py) {
        if (widthScale < 0) {
            return;
        }
        if (heightScale < 0) {
            return;
        }
        Matrix matrix = new Matrix();
        matrix.setScale(widthScale, heightScale, px, py);
        if (null == this.transformTrack) {
            this.transformTrack = new Path();
        }
        this.track.transform(matrix, this.transformTrack);
    }

    public void rotate(float degrees) {
        rotate(degrees, getWidth() / 2, getHeight() / 2);
    }

    /**
     * 當(dāng)發(fā)生旋轉(zhuǎn)時(shí)被調(diào)用
     *
     * @param degrees 旋轉(zhuǎn)的角度
     * @param px      旋轉(zhuǎn)中心點(diǎn)的x軸坐標(biāo)
     * @param py      旋轉(zhuǎn)中心點(diǎn)的y軸坐標(biāo)
     */
    public void rotate(float degrees, float px, float py) {
        Matrix matrix = new Matrix();
        matrix.setRotate(degrees, px, py);
        if (null == this.transformTrack) {
            this.transformTrack = new Path();
        }
        this.track.transform(matrix, this.transformTrack);
    }

    /**
     * 當(dāng)發(fā)生平移時(shí)被調(diào)用
     *
     * @param dx 橫向平移的距離
     * @param dy 縱向平移的距離
     */
    public void translate(float dx, float dy) {
        Matrix matrix = new Matrix();
        matrix.setTranslate(dx, dy);
        if (null == this.transformTrack) {
            this.transformTrack = new Path();
        }
        this.track.transform(matrix, this.transformTrack);
    }

    @Override
    public void draw(Canvas canvas, Paint paint) {
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(thickness);
        if (null != pathEffect) {
            paint.setPathEffect(pathEffect);
        }
        if (null == transformTrack) {
            canvas.drawPath(track, paint);
        } else {
            canvas.drawPath(transformTrack, paint);
        }
    }

    public Path getTrack() {
        return this.track;
    }

    public void setTrack(Path track) {
        this.track = track;
    }
}

/**
 * 用來(lái)繪制圓環(huán)的軌跡圖形
 */
public class RingTrackShape extends TrackShape {

    private float centerX = 0;
    private float centerY = 0;

    /**
     * 內(nèi)圓的半徑
     */
    private float innerRadius = 0;


    /**
     * 弧形起始角度
     */
    private float startAngle = 0;

    /**
     * 弧形掃過(guò)的角度(正數(shù)代表順時(shí)針掃, 負(fù)數(shù)代表逆時(shí)針掃)
     */
    private float sweepAngle = 0;

    public RingTrackShape(float thickness, float startAngle, float sweepAngle) {
        this(thickness, startAngle, sweepAngle, null);
    }

    public RingTrackShape(float thickness, float startAngle, float sweepAngle, DashPathEffect dashPathEffect) {
        super(null, thickness, dashPathEffect);
        this.startAngle = startAngle;
        this.sweepAngle = sweepAngle;
    }

    @Override
    protected void onResize(float width, float height) {
        super.onResize(width, height);
        this.centerX = width / 2;
        this.centerY = height / 2;
        this.innerRadius = Math.min(centerX - thickness, centerY - thickness);
        this.track = new Path();
        this.track.addArc(new RectF(centerX -  innerRadius - thickness/ 2,
                centerY - innerRadius - thickness / 2,
                centerX + innerRadius + thickness / 2,
                centerY + innerRadius + thickness / 2), startAngle, sweepAngle);
    }
}

D围肥、E層的圓環(huán)是通過(guò)上面的RingTrackShape繪制的置尔,F(xiàn)層的對(duì)勾是通過(guò)TrackShape繪制的。

通過(guò)屬性動(dòng)畫(huà)讓D氢伟、E榜轿、F層中的圓環(huán)和對(duì)勾動(dòng)起來(lái)

上面提到的TrackShape和RingTrackShape將繪制圓環(huán)和對(duì)勾的過(guò)程進(jìn)行了封裝,然后在自定義的TrackShapeDrawable中對(duì)TrackShape和RingTrackShape進(jìn)行繪制朵锣,說(shuō)到自定義的Drawable谬盐,必須要了解一下Drawable類(lèi):

/**
 * 指定drawable的alpha值。 0表示完全透明诚些,255表示完全不透明飞傀。
 */
public abstract void setAlpha(@IntRange(from=0,to=255) int alpha);

/**
 * 為drawable指定可選的color filter。
 *
 * 如果Drawable具有ColorFilter诬烹,則Drawables繪圖內(nèi)容的每個(gè)輸出像素在被混合到
 * Canvas的render目標(biāo)之前將被color filter進(jìn)行修改砸烦。
 *
 * 通過(guò)傳遞null參數(shù)刪除任何現(xiàn)有的color filter。
 * 注意: 設(shè)置非null的color filter將使{@link #setTintList(ColorStateList)tint}無(wú)效绞吁。
 * 
 * @param colorFilter 要應(yīng)用的color filter幢痘,或null參數(shù)刪除現(xiàn)有的彩色濾鏡
 */
public abstract void setColorFilter(@Nullable ColorFilter colorFilter);

/**
 * 返回Drawable的不透明度。 返回的值是如下常量之一:
 * {@link android.graphics.PixelFormat}:
 * {@link android.graphics.PixelFormat#UNKNOWN},
 * {@link android.graphics.PixelFormat#TRANSLUCENT},
 * {@link android.graphics.PixelFormat#TRANSPARENT}, or
 * {@link android.graphics.PixelFormat#OPAQUE}.
 *
 * OPAQUE drawable是在其bounds范圍內(nèi)繪制所有內(nèi)容家破,完全覆蓋了drawable后面的任何內(nèi)容颜说。 
 * TRANSPARENT drawable是在其bounds范圍內(nèi)不繪制任何內(nèi)容的购岗,可以讓其后面的任何東西顯示出來(lái)。
 * TRANSLUCENT drawable是在其bounds范圍內(nèi)繪制一些但不是全部的內(nèi)容门粪,并且至少在drawable后面的一些內(nèi)容將可見(jiàn)藕畔。 
 * 如果無(wú)法確定可繪制內(nèi)容的可見(jiàn)性,則最安全/最佳返回值為T(mén)RANSLUCENT庄拇。
 *
 * 一般來(lái)說(shuō),返回值應(yīng)該盡可能保守(使用resolveOpacity方法可以做到)韭邓。
 * 例如措近,如果它包含多個(gè)child drawables,并且一次僅顯示其中一個(gè)女淑,如果只有一個(gè)子項(xiàng)是TRANSLUCENT瞭郑,
 * 而其他子項(xiàng)是OPAQUE,則應(yīng)返回TRANSLUCENT鸭你。
 *
 * 請(qǐng)注意屈张,返回的值不一定考慮通過(guò)setAlpha或setColorFilter方法應(yīng)用的自定義Alpha或color filter。
 * 某些子類(lèi)(例如 BitmapDrawable袱巨,ColorDrawable和GradientDrawable)確實(shí)存在setAlpha的值阁谆,
 * 但一般行為取決于子類(lèi)的實(shí)現(xiàn)。
 *
 * @return int  Drawable的不透明度類(lèi)型愉老。
 *
 * @see android.graphics.PixelFormat
 */
public abstract @PixelFormat.Opacity int getOpacity();

/**
 * 再其邊界內(nèi)繪制(通過(guò)setBounds設(shè)置)场绿,可以使用alpha(通過(guò)setAlpha設(shè)置)和
 * color filter(通過(guò)setColorFilter設(shè)置)等可選效果。
 *
 * @param canvas The canvas to draw into
 */
public abstract void draw(@NonNull Canvas canvas);

接著來(lái)看一下TrackShapeDrawable是如何對(duì)TrackShape和RingTrackShape進(jìn)行繪制的:

/**
 * 用于繪制TrackShape的Drawable
 */
public class TrackShapeDrawable extends Drawable {

    /**
     * 對(duì)應(yīng)要繪制的軌跡圖形
     */
    protected TrackShape shape = null;

    protected Paint paint;

    public TrackShapeDrawable(TrackShape shape) {
        this.shape = shape;
        this.paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
    }

    public Paint getPaint() {
        return paint;
    }

    public Path getTrack() {
        if (null == shape) {
            return null;
        }
        return shape.getTrack();
    }

    public void setTrack(Path track) {
        if (null == shape || null == track) {
            return;
        }
        shape.setTrack(track);
        invalidateSelf();
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        super.onBoundsChange(bounds);
        updateShape(bounds);
    }

    private void updateShape(Rect bounds) {
        if (null != shape) {
            shape.resize(bounds.width(), bounds.height());
        }
    }

    public void scale(float widthScale, float heightScale) {
        if (null == shape) {
            return;
        }
        shape.scale(widthScale, heightScale);
        invalidateSelf();
    }

    /**
     * 當(dāng)發(fā)生縮放時(shí)被調(diào)用
     *
     * @param widthScale  橫向縮放的比例
     * @param heightScale 縱向縮放的比例
     * @param px          縮放中心點(diǎn)的x軸坐標(biāo)
     * @param py          縮放中心點(diǎn)的y軸坐標(biāo)
     */
    public void scale(float widthScale, float heightScale, float px, float py) {
        if (null == shape) {
            return;
        }
        shape.scale(widthScale, heightScale, px, py);
        invalidateSelf();
    }

    public void rotate(float degrees) {
        if (null == shape) {
            return;
        }
        shape.rotate(degrees);
        invalidateSelf();
    }

    /**
     * 當(dāng)發(fā)生旋轉(zhuǎn)時(shí)被調(diào)用
     *
     * @param degrees 旋轉(zhuǎn)的角度
     * @param px      旋轉(zhuǎn)中心點(diǎn)的x軸坐標(biāo)
     * @param py      旋轉(zhuǎn)中心點(diǎn)的y軸坐標(biāo)
     */
    public void rotate(float degrees, float px, float py) {
        if (null == shape) {
            return;
        }
        shape.rotate(degrees, px, py);
        invalidateSelf();
    }

    /**
     * 當(dāng)發(fā)生平移時(shí)被調(diào)用
     *
     * @param dx 橫向平移的距離
     * @param dy 縱向平移的距離
     */
    public void translate(float dx, float dy) {
        if (null == shape) {
            return;
        }
        shape.translate(dx, dy);
        invalidateSelf();
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        if (null == shape) {
            return;
        }
        shape.draw(canvas, paint);
    }

    @Override
    public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
        this.paint.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {
        this.paint.setColorFilter(colorFilter);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }
}

上面代碼中的看出scale嫉入、rotate焰盗、translate、setTrack方法是用來(lái)實(shí)現(xiàn)對(duì)TrackShape和RingTrackShape中的軌跡進(jìn)行scale咒林、rotate熬拒、translate和替換,具體可以參考TrackShape中的scale垫竞、rotate澎粟、translate、setTrack方法件甥。講到這里捌议,大家應(yīng)該有了實(shí)現(xiàn)動(dòng)畫(huà)的思路了吧,就是通過(guò)屬性動(dòng)畫(huà)來(lái)動(dòng)態(tài)調(diào)用TrackShapeDrawable的scale引有、rotate瓣颅、translate、setTrack方法即可譬正。

接下來(lái)為了實(shí)現(xiàn)D宫补、E檬姥、F層中的圓環(huán)和對(duì)勾對(duì)應(yīng)的縮放動(dòng)畫(huà)和軌跡動(dòng)畫(huà),我實(shí)現(xiàn)了ScaleAnimatorDrawable粉怕、TrackAnimatorDrawable兩個(gè)自定義Drawable健民,這兩個(gè)自定義Drawable就是對(duì) 屬性動(dòng)畫(huà)動(dòng)態(tài)調(diào)用TrackShapeDrawable的scale、setTrack方法 的封裝贫贝,其中ScaleAnimatorDrawable不僅支持TrackShapeDrawable也支持其它類(lèi)型的Drawable秉犹,實(shí)現(xiàn)代碼如下:

/**
 * 作為自定義動(dòng)畫(huà)Drawable的基類(lèi)
 */
public abstract class AnimatorDrawable extends Drawable implements Animatable, Drawable.Callback, Animator.AnimatorListener {

    /**
     * 在Drawable繪制區(qū)域的基礎(chǔ)上設(shè)置padding
     */
    Rect padding = new Rect();

    /**
     * 要被動(dòng)畫(huà)的drawable
     */
    protected Drawable drawable = null;

    private ValueAnimator valueAnimator = null;

    /**
     * 表示動(dòng)畫(huà)是不是已經(jīng)被啟動(dòng)
     */
    private boolean isFirst = true;

    /**
     * 軌跡動(dòng)畫(huà)的時(shí)長(zhǎng),默認(rèn)200毫秒
     */
    protected long duration = 200;

    /**
     * 軌跡動(dòng)畫(huà)延遲執(zhí)行的時(shí)長(zhǎng),默認(rèn)為0,即不延遲
     */
    protected long startDelay = 0;

    /**
     * 代表動(dòng)畫(huà)執(zhí)行完成之后是否停留在最后一幀
     * true  代表停留在最后一幀
     * false 代表不會(huì)顯示如何東西
     */
    private boolean fillAfter = true;

    public AnimatorDrawable(Drawable drawable, long duration) {
        this(drawable, duration, true);
    }

    public AnimatorDrawable(Drawable drawable, long duration, boolean fillAfter) {
        if (null == drawable) {
            return;
        }
        this.drawable = drawable;
        this.drawable.setCallback(this);// 防止drawable的invalidateSelf方法無(wú)法重繪
        if (duration > 0) {
            this.duration = duration;
        }
        this.fillAfter = fillAfter;
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        super.onBoundsChange(bounds);
        updateDrawable();
    }

    private void updateDrawable() {
        if (null == drawable) {
            return;
        }

        Rect bounds = getBounds();
        if (null == padding) {
            drawable.setBounds(bounds);
        } else {
            drawable.setBounds(bounds.left, bounds.top,
                    bounds.right - padding.left - padding.right, bounds.bottom - padding.top - padding.bottom);
        }
    }

    /**
     * Sets padding for this shape, defined by a Rect object. Define the padding
     * in the Rect object as: left, top, right, bottom.
     */
    public void setPadding(Rect padding) {
        if (padding == null) {
            return;
        }
        this.padding = padding;
        updateDrawable();
        invalidateSelf();
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        if (isFirst) {
            start();
            isFirst = false;
        } else {
            if (isRunning() || null == valueAnimator) {
                onDraw(canvas);
            }
        }
    }

    public void onDraw(@NonNull Canvas canvas) {
        if (null != padding) {
            canvas.save();
            canvas.translate(padding.left, padding.top);
            canvas.clipRect(0, 0, drawable.getBounds().width(), drawable.getBounds().height());
            drawTrack(canvas);
            canvas.restore();
        } else {
            canvas.save();
            drawTrack(canvas);
            canvas.restore();
        }
    }

    public abstract void drawTrack(@NonNull Canvas canvas);

    /**
     * 獲取軌跡動(dòng)畫(huà)的時(shí)長(zhǎng)
     */
    public long getDuration() {
        return duration;
    }

    public void setStartDelay(long startDelay) {
        if (startDelay < 0) {
            return;
        }
        this.startDelay = startDelay;
    }

    public long getStartDelay() {
        return startDelay;
    }

    void setValueAnimator(ValueAnimator valueAnimator) {
        if (null == valueAnimator) {
            this.valueAnimator.cancel();
            this.valueAnimator = null;
        } else {
            this.valueAnimator = valueAnimator;
        }
    }

    @Override
    public void start() {
        if (null != valueAnimator && (valueAnimator.isRunning() || valueAnimator.isStarted())) {
            valueAnimator.cancel();
            valueAnimator = null;
        }
        setupAnimators();
        if (null != valueAnimator) {
            valueAnimator.start();
        }
    }

    public abstract void setupAnimators();

    @Override
    public void stop() {
        if (null != valueAnimator && (valueAnimator.isRunning() || valueAnimator.isStarted())) {
            valueAnimator.cancel();
        }
    }

    @Override
    public boolean isRunning() {
        return null != valueAnimator && valueAnimator.isRunning();
    }

    @Override
    public void invalidateDrawable(@NonNull Drawable who) {
        invalidateSelf();
    }

    @Override
    public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
        scheduleSelf(what, when);
    }

    @Override
    public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
        unscheduleSelf(what);
    }

    @Override
    public void onAnimationStart(Animator animation) {
        if (null != this.onAnimatorListener) {
            this.onAnimatorListener.onAnimationStart(this);
        }
    }

    @Override
    public void onAnimationEnd(Animator animation) {
        valueAnimator.removeAllUpdateListeners();
        valueAnimator.removeAllListeners();
        if (fillAfter) {
            setValueAnimator(null);
        }
        if (null != this.onAnimatorListener) {
            this.onAnimatorListener.onAnimationEnd(this);
        }
    }

    @Override
    public void onAnimationCancel(Animator animation) {

    }

    @Override
    public void onAnimationRepeat(Animator animation) {

    }

    @Override
    public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {}

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {}

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    private OnAnimatorListener onAnimatorListener = null;

    public void setOnAnimatorListener(OnAnimatorListener onAnimatorListener) {
        this.onAnimatorListener = onAnimatorListener;
    }

    public interface OnAnimatorListener {
        void onAnimationStart(AnimatorDrawable animatorDrawable);
        void onAnimationEnd(AnimatorDrawable animatorDrawable);
    }
}

/**
 * 對(duì)Drawable進(jìn)行Scale動(dòng)畫(huà)
 */
public class ScaleAnimatorDrawable extends AnimatorDrawable {

    /**
     * 代表Drawable縮放的比例
     */
    private float scale = 0;

    private Rect initPadding = new Rect();


    public ScaleAnimatorDrawable(Drawable drawable, long duration) {
        super(drawable, duration);
    }

    @Override
    public void setupAnimators() {
        // 在啟動(dòng)動(dòng)畫(huà)之前記錄最開(kāi)始的padding
        initPadding = padding;

        final ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 1f);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                scale = (float) animation.getAnimatedValue();
                preDraw();
            }
        });
        valueAnimator.addListener(this);
        valueAnimator.setDuration(duration).setStartDelay(startDelay);
        setValueAnimator(valueAnimator);
    }

    private void preDraw() {
        if (drawable instanceof TrackShapeDrawable) {
            ((TrackShapeDrawable) drawable).scale(scale, scale);
        } else {
            Rect bounds = getBounds();
            setPadding(new Rect((int) (initPadding.left + (bounds.width() - initPadding.left - initPadding.right) * 0.5 * (1 - scale)),
                    (int) (initPadding.top + (bounds.height() - initPadding.top - initPadding.bottom) * 0.5 * (1 - scale)),
                    (int) (initPadding.right + (bounds.width() - initPadding.left - initPadding.right) * 0.5 * (1 - scale)),
                    (int) (initPadding.bottom + (bounds.height() - initPadding.top - initPadding.bottom) * 0.5 * (1 - scale))));
        }
    }

    @Override
    public void drawTrack(@NonNull Canvas canvas) {
        drawable.draw(canvas);
    }
}

/**
 * 對(duì)TrackShapeDrawable進(jìn)行軌跡動(dòng)畫(huà)
 */
public class TrackAnimatorDrawable extends AnimatorDrawable {

    /**
     * 代表將要被繪制的軌跡片段
     */
    private Path trackSegment = null;

    public TrackAnimatorDrawable(TrackShapeDrawable drawable, long duration) {
        super(drawable, duration);
    }

    @Override
    public void setupAnimators() {
        final ValueAnimator valueAnimator = ValueAnimator.ofObject(new PathEvaluator(), new Path(), ((TrackShapeDrawable) drawable).getTrack());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                trackSegment = (Path) animation.getAnimatedValue();
                preDraw();
            }
        });
        valueAnimator.addListener(this);
        valueAnimator.setDuration(duration).setStartDelay(startDelay);
        setValueAnimator(valueAnimator);
        trackSegment = new Path();
    }

    private void preDraw() {
        ((TrackShapeDrawable) drawable).setTrack(trackSegment);
    }

    @Override
    public void drawTrack(@NonNull Canvas canvas) {
        drawable.draw(canvas);
    }
}

接著就是使用上面自定義的ScaleAnimatorDrawable稚晚、TrackAnimatorDrawable實(shí)現(xiàn)動(dòng)畫(huà)效果了:

// A層除了顏色值變?yōu)?xAAFFFFFF其它和2>中相同崇堵,B層也和2>中相同,這里不再贅敘客燕。

// C層
float radius2 = DisplayUtils.dip2px(this, 19f);
GradientDrawable smallBgCircleCenter = new GradientDrawable();
radii = new float[]{radius2, radius2, radius2, radius2,
        radius2, radius2, radius2, radius2};
smallBgCircleCenter.setColor(0xDDFFFFFF);
smallBgCircleCenter.setCornerRadii(radii);
PaddingDrawable smallBgCircle = new PaddingDrawable(smallBgCircleCenter);
int padding3 = DisplayUtils.dip2px(this, 19f);
smallBgCircle.setPadding(new Rect(padding3, padding3, padding3, padding3));

// D層
ScaleAnimatorDrawable scaledCircle = (ScaleAnimatorDrawable) AnimatorDrawableFactory.createAnimatorDrawable(this, getResources().getDrawable(R.drawable.ic_drawable_ring), AnimatorDrawableFactory.AnimatorDrawableType.SCALE);
int padding4 = DisplayUtils.dip2px(this, 19f);
scaledCircle.setPadding(new Rect(padding4, padding4, padding4, padding4));

// E層
TrackShapeDrawable scaleRingDrawable = new TrackShapeDrawable(new RingTrackShape(DisplayUtils.dip2px(this, 2), 0, 360));
scaleRingDrawable.getPaint().setColor(Color.GREEN);
TrackAnimatorDrawable trackAnimatorDrawable = (TrackAnimatorDrawable) AnimatorDrawableFactory.createAnimatorDrawable(this, scaleRingDrawable, AnimatorDrawableFactory.AnimatorDrawableType.TRACK);
int padding5 = DisplayUtils.dip2px(this, 20f);
trackAnimatorDrawable.setPadding(new Rect(padding5, padding5, padding5, padding5));

// F層
Path track = new Path();
track.moveTo(0f, DisplayUtils.dip2px(this, 10f));
track.lineTo(DisplayUtils.dip2px(this, 10f), DisplayUtils.dip2px(this, 20f));
track.lineTo(DisplayUtils.dip2px(this, 20f), 0f);
TrackShape trackShape = new TrackShape(track, DisplayUtils.dip2px(this, 2));
TrackShapeDrawable trackShapeDrawable3 = new TrackShapeDrawable(trackShape);
trackShapeDrawable3.getPaint().setColor(Color.GREEN);
ScaleAnimatorDrawable scaleAnimatorDrawable = (ScaleAnimatorDrawable) AnimatorDrawableFactory.createAnimatorDrawable(this, trackShapeDrawable3, AnimatorDrawableFactory.AnimatorDrawableType.SCALE);
int padding6 = DisplayUtils.dip2px(this, 28f);
scaleAnimatorDrawable.setPadding(new Rect(padding6, padding6, padding6, padding6));

TrackAnimatorManager trackAnimatorManager = new TrackAnimatorManager();
trackAnimatorManager.setOnAnimatorListener(this);
LayerDrawable layerDrawable = trackAnimatorManager.createLayerDrawable(TrackAnimatorManager.Order.SEQUENTIALLY,
        bigBgCircle, clock, smallBgCircle, scaledCircle, trackAnimatorDrawable, scaleAnimatorDrawable);
animatorDrawableView.setBackground(layerDrawable);

上面代碼中的trackAnimatorManager.createLayerDrawable是將多個(gè)Drawable放到一個(gè)LayerDrawable中鸳劳,如果其中有AnimatorDrawable的子類(lèi)就會(huì)將其按照其在數(shù)組中順序依次進(jìn)行動(dòng)畫(huà),實(shí)現(xiàn)代碼如下:

public LayerDrawable createLayerDrawable(Order order, Drawable... drawables) {
    if (null == drawables || drawables.length <= 0) {
        return null;
    }
    this.drawables = drawables;
    LayerDrawable layerDrawable = new LayerDrawable(this.drawables);
    int startDelay = 0;
    for (Drawable drawable : this.drawables) {
        if (drawable instanceof AnimatorDrawable) {
            startDelay += ((AnimatorDrawable) drawable).getStartDelay();
            ((AnimatorDrawable) drawable).setOnAnimatorListener(this);
            switch (order) {
                case SEQUENTIALLY: {
                    ((AnimatorDrawable) drawable).setStartDelay(startDelay);
                    break;
                }
            }
            startDelay += ((AnimatorDrawable) drawable).getDuration();
        }
    }
    return layerDrawable;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末也搓,一起剝皮案震驚了整個(gè)濱河市赏廓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌傍妒,老刑警劉巖幔摸,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異拍顷,居然都是意外死亡抚太,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)昔案,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)尿贫,“玉大人,你說(shuō)我怎么就攤上這事踏揣∏焱觯” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵捞稿,是天一觀的道長(zhǎng)又谋。 經(jīng)常有香客問(wèn)我,道長(zhǎng)娱局,這世上最難降的妖魔是什么彰亥? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮衰齐,結(jié)果婚禮上任斋,老公的妹妹穿的比我還像新娘。我一直安慰自己耻涛,他們只是感情好废酷,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布瘟檩。 她就那樣靜靜地躺著,像睡著了一般澈蟆。 火紅的嫁衣襯著肌膚如雪墨辛。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天趴俘,我揣著相機(jī)與錄音睹簇,去河邊找鬼。 笑死寥闪,一個(gè)胖子當(dāng)著我的面吹牛带膀,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播橙垢,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼伦糯!你這毒婦竟也來(lái)了柜某?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤敛纲,失蹤者是張志新(化名)和其女友劉穎喂击,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體淤翔,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡翰绊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了旁壮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片监嗜。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖抡谐,靈堂內(nèi)的尸體忽然破棺而出裁奇,到底是詐尸還是另有隱情,我是刑警寧澤麦撵,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布刽肠,位于F島的核電站,受9級(jí)特大地震影響免胃,放射性物質(zhì)發(fā)生泄漏音五。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一羔沙、第九天 我趴在偏房一處隱蔽的房頂上張望躺涝。 院中可真熱鬧,春花似錦撬碟、人聲如沸诞挨。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)惶傻。三九已至棍郎,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間银室,已是汗流浹背涂佃。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蜈敢,地道東北人辜荠。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像抓狭,于是被迫代替她去往敵國(guó)和親伯病。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容