最近跟小夥伴一塊兒討論了一下,決定一塊兒仿一個BiliBili的app(包括android端和iOS端),咱們並無打算把這個項目徹底作完,畢竟咱們的重點是掌握一些新框架的使用,並在實戰過程當中發現並彌補自身的不足。java
本系列將記錄我(android端)在開發過程當中的一些我以爲有必要記錄的功能實現而已,並非完整的從0到1的完整教程,若個別看官大爺以爲很差請出門左拐謝謝。android
如下是該項目將會完成的功能。git
本系列文章,將會有記錄以上功能的實現但不只僅只有這些,還會有一些其餘,好比自定義控件、利用fiddler抓包等,接下來就進入本篇的主題——《仿bilibili刷新按鈕的實現》。github
先來看看原版效果:canvas
該按鈕由3部分組成,分別是圓角矩形、文字、旋轉圖標。在點擊按鈕後,開始加載數據,旋轉圖標發生旋轉,數據加載完成後,旋轉圖標復位並中止旋轉。話很少說,開始敲代碼。bash
這裏,咱們要繪製的部分有3個,分別是上面提到的圓角矩形、文字、旋轉圖標。那麼這裏就爲這3部分分別聲明瞭一些屬性。app
要注意的一點是,這個類中有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);
// 一、畫圓角矩形
// 二、畫字
// 三、畫刷新圖標
}
}複製代碼
接下來着重完成onDraw()方法的實現:ide
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 一、畫圓角矩形
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(borderColor);
mPaint.setStrokeWidth(borderWidth);
canvas.drawRoundRect(new RectF(0, 0, getWidth(), getHeight()), borderRadius, borderRadius, mPaint);
// 二、畫字
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);
// 三、畫刷新圖標
float iconStartX = textStartX + measureText + space4TextAndIcon;
canvas.drawBitmap(iconBitmap, iconStartX, getHeight() / 2 - iconSize / 2, mPaint);
}複製代碼
先來看看效果:函數
我給該控件設置了寬爲200dp,高爲100dp。
能夠看到效果還不錯,但仍是有一點點問題的,下面就分別說說這3部分是怎麼畫的,及存在的小問題。
其實畫圓角矩形很簡單,設置好畫筆的樣式、顏色、線粗,再調用canvas的drawRoundRect()方法便可實現。
但你有沒有發現,此時的 線粗爲0(borderWidth=0),矩形線怎麼還有?這是由於畫筆的樣式爲Paint.Style.STROKE,當線粗爲0時,還要畫出1px的線,由於對畫筆來講,最小的線粗就是1px。因此,上面的代碼須要作以下改動:
// 一、畫圓角矩形
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個最爲重要。
如上圖中,如今要得到的就是文字左下角的點,這要怎麼求呢?
先說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;複製代碼
最後就是畫刷新圖標了,它是以左上角爲起點的,經過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;
}
...
}複製代碼
如今,要實現旋轉圖標的旋轉功能了。原理就是在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);
...
// 三、畫刷新圖標
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的使用也不復雜,這裏就不詳細介紹了。來看下動畫效果吧:
一個自定義控件,是不能把屬性值寫死在控件裏的,因此咱們須要自定義屬性,從外界獲取這些屬性值。
在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>複製代碼
在控件的第三個構造函數中獲取這些屬性值:
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。
<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"/>複製代碼