專案實戰 - 仿 bilibili 重新整理按鈕的實現

GitLqr發表於2017-10-24

一、簡述

最近跟小夥伴一起討論了一下,決定一起仿一個BiliBili的app(包括android端和iOS端),我們並沒有打算把這個專案完全做完,畢竟我們的重點是掌握一些新框架的使用,並在實戰過程中發現並彌補自身的不足。

本系列將記錄我(android端)在開發過程中的一些我覺得有必要記錄的功能實現而已,並不是完整的從0到1的完整教程,若個別看官大爺覺得不好請出門左拐謝謝。

以下是該專案將會完成的功能。

  1. 視訊播放功能
  2. 直播功能
  3. 彈幕功能
  4. 換膚功能
  5. ...

本系列文章,將會有記錄以上功能的實現但不僅僅只有這些,還會有一些其他,比如自定義控制元件、利用fiddler抓包等,接下來就進入本篇的主題——《仿bilibili重新整理按鈕的實現》。

二、實戰

1、分析

先來看看原版效果:

該按鈕由3部分組成,分別是圓角矩形、文字、旋轉圖示。在點選按鈕後,開始載入資料,旋轉圖示發生旋轉,資料載入完成後,旋轉圖示復位並停止旋轉。話不多說,開始敲程式碼。

2、繪製

這裡,我們要繪製的部分有3個,分別是上面提到的圓角矩形、文字、旋轉圖示。那麼這裡就為這3部分分別宣告瞭一些屬性。

要注意的一點是,這個類中有3個建構函式,因為有部分屬性需要在建構函式中初始化(也為之後自定義屬性做準備),所以,將第1個與第2個建構函式中的super修改為this。

public class LQRRefreshButton extends View {

    // 圓角矩形屬性
    private int borderColor = Color.parseColor("#fb7299");
    private float borderWidth = 0;
    private float borderRadius = 120;

    // 文字屬性
    private String text = "點選換一批";
    private int textColor = Color.parseColor("#fb7299");
    private float textSize = 28;

    // 旋轉圖示屬性
    private int iconSrc = R.mipmap.tag_center_refresh_icon;
    private float iconSize = 28;
    private Bitmap iconBitmap;
    private float space4TextAndIcon = 20;

    // 畫筆
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    public LQRRefreshButton(Context context) {
        this(context, null);
    }

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

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

        // 將圖示資源例項化為Bitmap
        iconBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.tag_center_refresh_icon);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 1、畫圓角矩形
        // 2、畫字
        // 3、畫重新整理圖示
    }
}複製程式碼

接下來著重完成onDraw()方法的實現:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    // 1、畫圓角矩形
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setColor(borderColor);
    mPaint.setStrokeWidth(borderWidth);
    canvas.drawRoundRect(new RectF(0, 0, getWidth(), getHeight()), borderRadius, borderRadius, mPaint);

    // 2、畫字
    mPaint.setTextSize(textSize);
    mPaint.setColor(textColor);
    mPaint.setStyle(Paint.Style.FILL);
    float measureText = mPaint.measureText(text);
    float measureAndIcon = measureText + space4TextAndIcon + iconSize;
    float textStartX = getWidth() / 2 - measureAndIcon / 2;
    float textBaseY = getHeight() / 2 + (Math.abs(mPaint.ascent()) - mPaint.descent()) / 2;
    canvas.drawText(text, textStartX, textBaseY, mPaint);

    // 3、畫重新整理圖示
    float iconStartX = textStartX + measureText + space4TextAndIcon;
    canvas.drawBitmap(iconBitmap, iconStartX, getHeight() / 2 - iconSize / 2, mPaint);
}複製程式碼

先來看看效果:

我給該控制元件設定了寬為200dp,高為100dp。

可以看到效果還不錯,但還是有一點點問題的,下面就分別說說這3部分是怎麼畫的,及存在的小問題。

1)畫圓角矩形

其實畫圓角矩形很簡單,設定好畫筆的樣式、顏色、線粗,再呼叫canvas的drawRoundRect()方法即可實現。

  • 因為我們要畫的圓角矩形只需要畫線,所以畫筆的樣式便設定為Paint.Style.STROKE。
  • canvas的drawRoundRect()方法中,第一個引數是繪製範圍,這裡就直接按該控制元件的大小來設定即可。第二、三個引數是x軸和y軸的圓角半徑,第三個引數是畫筆(要畫東西當然需要畫筆~)。

但你有沒有發現,此時的 線粗為0(borderWidth=0),矩形線怎麼還有?這是因為畫筆的樣式為Paint.Style.STROKE,當線粗為0時,還要畫出1px的線,因為對畫筆來說,最小的線粗就是1px。所以,上面的程式碼需要做如下改動:

// 1、畫圓角矩形
if (borderWidth > 0) {
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setColor(borderColor);
    mPaint.setStrokeWidth(borderWidth);
    canvas.drawRoundRect(new RectF(0, 0, getWidth(), getHeight()), borderRadius, borderRadius, mPaint);
}複製程式碼

2)畫字

畫字的一般步驟是設定文字大小、文字顏色、畫筆樣式,繪製起點。其中後2個最為重要。

  • 畫筆樣式對畫出的字是有影響的,當畫筆樣式為Paint.Style.STROKE時,畫出來的字是鏤空的(不信你可以試試),我們需要的是實心的字,所以需要修改畫筆的樣式為Paint.Style.FILL。
  • 在安卓中,文字的繪製跟其它繪製是不同的,例如,圓角矩形和旋轉圖示的繪製起點是左上角,而文字則是按文字左下字為起點,也就是按基線(Baseline)來繪製,故需要得到基線起點的座標。

如上圖中,現在要獲得的就是文字左下角的點,這要怎麼求呢?

先說x,一般需要讓文字居中顯示(跟文字的對齊方式也有關係,這裡以預設的左對齊為例),所以計算公式一般為: x = 控制元件寬度/2 - 文字長度/2。但我們這個控制元件有點不同,它還需要考慮到旋轉圖示的位置問題,所以x應該這麼求: x = 控制元件寬度/2 - (文字長度+空隙+旋轉圖示寬度)/2

// 得到文字長度
float measureText = mPaint.measureText(text);
// 得到 文字長度+空隙+旋轉圖示寬度
float measureAndIcon = measureText + space4TextAndIcon + iconSize;
// 得到文字繪製起點
float textStartX = getWidth() / 2 - measureAndIcon / 2;複製程式碼

再說y,如圖所示:

如果直接用控制元件的高度的一半作為文字繪製的基線,那麼繪製出來的文字肯定偏上,這是因為Ascent的高度比Descent的高度要高的多,我們在計算Baseline時,需要在Ascent中減去Descent的高度得到兩者高度差,再讓控制元件中心y座標加上(下降)這個高度差的一半。故:

float textBaseY = getHeight() / 2 + (Math.abs(mPaint.ascent()) - mPaint.descent()) / 2;複製程式碼

3)畫重新整理圖示

最後就是畫重新整理圖示了,它是以左上角為起點的,通過canvas的drawBitmap()方法進行繪製即可。

但是,有一點需要注意,iconSize是我自己定的一個大小,並不是圖示的實際大小,所以在往後做旋轉動畫時獲取到的旋轉中心會有誤差,將導致圖示旋轉時不是按中心進行旋轉。所以,這裡需要對圖示大小進行調整:

public class LQRRefreshButton extends View {
    ...
    public LQRRefreshButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        // icon
        iconBitmap = BitmapFactory.decodeResource(getResources(), iconSrc);
        iconBitmap = zoomImg(iconBitmap, iconSize, iconSize);
    }

    public Bitmap zoomImg(Bitmap bm, float newWidth, float newHeight) {
        // 獲得圖片的寬高
        int width = bm.getWidth();
        int height = bm.getHeight();
        // 計算縮放比例
        float scaleWidth = ((float) newWidth) / width;
        float scaleHeight = ((float) newHeight) / height;
        // 取得想要縮放的matrix引數
        Matrix matrix = new Matrix();
        matrix.postScale(scaleWidth, scaleHeight);
        // 得到新的圖片
        Bitmap newbm = Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true);
        return newbm;
    }
    ...
}複製程式碼

3、動畫

現在,要實現旋轉圖示的旋轉功能了。原理就是在canvas繪製圖示時,將canvas進行旋轉,canvas旋轉著繪製圖示也很簡單,只需要4步:

canvas.save();
canvas.rotate(degress, centerX, centerY);
canvas.drawBitmap(iconBitmap, iconStartX, getHeight() / 2 - iconSize / 2, mPaint);
canvas.restore();複製程式碼

接下來要做的,就是計算出旋轉中心,旋轉角度,並不停止的去呼叫onDraw()編制圖示,可以使用ValueAnimator或ObjectAnimator實現這個功能,這裡選用ObjectAnimator。實現如下:

public class LQRRefreshButton extends View {
    ...
    private float degress = 0;
    private ObjectAnimator mAnimator;

    public LQRRefreshButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 旋轉動畫
        mAnimator = ObjectAnimator.ofObject(this, "degress", new FloatEvaluator(), 360, 0);
        mAnimator.setDuration(2000);
        mAnimator.setRepeatMode(ObjectAnimator.RESTART);
        mAnimator.setInterpolator(new LinearInterpolator());
        mAnimator.setRepeatCount(ObjectAnimator.INFINITE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        ...
        // 3、畫重新整理圖示
        float iconStartX = textStartX + measureText + space4TextAndIcon;
        canvas.save();
        float centerX = iconStartX + iconSize / 2;
        int centerY = getHeight() / 2;
        canvas.rotate(degress, centerX, centerY);
        canvas.drawBitmap(iconBitmap, iconStartX, getHeight() / 2 - iconSize / 2, mPaint);
        canvas.restore();
    }

    public void start() {
        mAnimator.start();
    }

    public void stop() {
        mAnimator.cancel();
        setDegress(0);
    }

    public float getDegress() {
        return degress;
    }

    public void setDegress(float degress) {
        this.degress = degress;
        invalidate();
    }
}複製程式碼

使用ObjectAnimator可以對任意屬性值進行修改,所以需要在該控制元件中宣告一個旋轉角度變數(degress),並編寫getter和setter方法,還需要在setter方法中呼叫invalidate(),這樣才能在角度值發生變換時,讓控制元件回撥onDraw()進行圖示的旋轉繪製。ObjectAnimator的使用也不復雜,這裡就不詳細介紹了。來看下動畫效果吧:

4、自定義屬性

一個自定義控制元件,是不能把屬性值寫死在控制元件裡的,所以我們需要自定義屬性,從外界獲取這些屬性值。

1)屬性檔案編寫

在attrs.xml中編寫如下程式碼:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="LQRRefreshButton">
        <attr name="refresh_btn_borderColor" format="color"/>
        <attr name="refresh_btn_borderWidth" format="dimension"/>
        <attr name="refresh_btn_borderRadius" format="dimension"/>
        <attr name="refresh_btn_text" format="string"/>
        <attr name="refresh_btn_textColor" format="color"/>
        <attr name="refresh_btn_textSize" format="dimension"/>
        <attr name="refresh_btn_iconSrc" format="reference"/>
        <attr name="refresh_btn_iconSize" format="dimension"/>
        <attr name="refresh_btn_space4TextAndIcon" format="dimension"/>
    </declare-styleable>
</resources>複製程式碼

2)屬性值獲取

在控制元件的第三個建構函式中獲取這些屬性值:

public class LQRRefreshButton extends View {

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

        // 獲取自定義屬性值
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LQRRefreshButton);
        borderColor = ta.getColor(R.styleable.LQRRefreshButton_refresh_btn_borderColor, Color.parseColor("#fb7299"));
        borderWidth = ta.getDimension(R.styleable.LQRRefreshButton_refresh_btn_borderWidth, dipToPx(0));
        borderRadius = ta.getDimension(R.styleable.LQRRefreshButton_refresh_btn_borderRadius, dipToPx(60));
        text = ta.getString(R.styleable.LQRRefreshButton_refresh_btn_text);
        if (text == null)
            text = "";
        textColor = ta.getColor(R.styleable.LQRRefreshButton_refresh_btn_textColor, Color.parseColor("#fb7299"));
        textSize = ta.getDimension(R.styleable.LQRRefreshButton_refresh_btn_textSize, spToPx(14));
        iconSrc = ta.getResourceId(R.styleable.LQRRefreshButton_refresh_btn_iconSrc, R.mipmap.tag_center_refresh_icon);
        iconSize = ta.getDimension(R.styleable.LQRRefreshButton_refresh_btn_iconSize, dipToPx(14));
        space4TextAndIcon = ta.getDimension(R.styleable.LQRRefreshButton_refresh_btn_space4TextAndIcon, dipToPx(10));

        ta.recycle();    
        ...
    }
}複製程式碼

這裡有一點需要留意:

ta.getDimension(屬性id, 預設值)複製程式碼

通過TypedArray物件可以從外界到的的值會根據單位(如:dp、sp)的不同自動轉換成px,但預設值的單位是一定的,為px,所以為了符合安卓規範,不要直接使用px,所以需要手動做個轉換。最後還需要呼叫recycle()方法回收TypedArray。

3)在佈局檔案中應用

<com.lqr.biliblili.mvp.ui.widget.LQRRefreshButton
    android:id="@+id/btn_refresh"
    android:layout_width="118dp"
    android:layout_height="32dp"
    android:layout_gravity="center"
    android:layout_marginBottom="3dp"
    android:layout_marginTop="8dp"
    app:refresh_btn_borderRadius="25dp"
    app:refresh_btn_borderWidth="1dp"
    app:refresh_btn_iconSize="16dp"
    app:refresh_btn_text="點選換一批"
    app:refresh_btn_textColor="@color/bottom_text_live"
    app:refresh_btn_textSize="14sp"/>複製程式碼

最後附近完整程式碼

LQRRefreshButton.java

相關文章