最近在作 App 的開屏頁,通常都是建立一個 SplashActivity 來展現 Logo 與廣告圖,因此這裏我也不例外,須要展現的圖片能夠是本地固定好的,也能夠是與服務器交互請求獲取到 Url 再進行加載,圖片請求加載很簡單,這裏就很少說了,接下來進入正題。android
原本我只是想實現網易雲音樂那樣的,如圖: canvas
Button
換個背景就好了,像網易雲音樂那樣的背景,本身用
drawable-shape
切一個圓角矩形便可。而有道的這個稍微複雜一點,既要有進度又要能點擊,普通的
ProgressBar
應該是實現不了的(由於普通的圓形
ProgressBar
是不肯定狀態,不知道這樣說對不對),因此我選擇自定義一個
ProgressBar
。
做爲開屏頁,那確定只是用來展現的(你也能夠在這裏初始化一些東西傳遞給下一個界面),既然如此那就須要在必定時間內進行跳轉了,這裏我是用handler.postDelayed()
設置延遲,在加載不到圖片的狀況下 2 秒後進行跳轉,若是加載到圖片了則調用removeCallbacksAndMessages()
來移除這個延時操做,當進度條走完時再進行跳轉,也能夠直接點擊{跳過}按鈕執行跳轉。話很少說,自定義 View 搞起來。bash
/**
* 一個圓形進度條,用於開屏廣告顯示進度
*
* @author Aaron Zheng
* @since 2019.04.19
*/
public class RoundProgressBar extends View {
// 這一塊做爲控件默認屬性,在使用者沒有對相應屬性進行賦值的狀況下
private static final int RING_COLOR = Color.parseColor("#4D000000");
private static final int PROGRESS_COLOR = Color.parseColor("#FFDDAE44");
private static final int RING_WIDTH = DisplayUtil.dp2px(3);
private static final int TEXT_SIZE = DisplayUtil.sp2px(10);
private static final int TEXT_COLOR = Color.WHITE;
private static final int WIDTH = DisplayUtil.dp2px(35);
private static final int MAX_PROGRESS = 100;
private static final int CUR_PROGRESS = 0;
private static final String TEXT = "跳過";
private static boolean sIsSkip = false; // 標記位,表示是否點擊了跳過
private int mRingColor; // 圓環顏色
private int mProgressColor; // 圓環進度條顏色
private int mRingWidth; // 圓環寬度
private int mTextSize; // 字體大小
private int mTextColor; // 字體顏色
private String mText; // 內容
private int mMaxProgress; // 最大進度
private int mCurProgress; // 當前進度
private Paint mRingPaint; // 圓環畫筆
private Paint mProgressPaint; // 圓環進度畫筆
private Paint mTextPaint; // 文字畫筆
public RoundProgressBar(Context context) {
this(context, null);
}
public RoundProgressBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public RoundProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 由於須要判斷 Activity 是否被銷燬,因此當使用者傳入
// 非 Activity 的 Context 時拋出一個異常
if (!(context instanceof Activity))
throw new IllegalArgumentException("Context must be activity.");
// 初始化 View 的參數
init(context, attrs);
}
/**
* 因爲是繼承自 View ,因此確定是須要重寫 onMeasure() 方法了
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int size = Math.min(measureSize(widthMeasureSpec), measureSize(heightMeasureSpec));
setMeasuredDimension(size, size);
}
@SuppressLint("DrawAllocation")
@Override
protected void onDraw(Canvas canvas) {
// draw ring
float circleX = (float) getWidth() / 2; // 肯定圓環的中心點
float circleY = (float) getWidth() / 2; // 肯定圓環的中心點
// 肯定半徑,須要注意的是圓環的寬度並不必定等於 View 的寬度,
// 由於環是有厚度的,在計算半徑時須要減去環寬度的一半
float radius = (float) getWidth() / 2 - (float) mRingWidth / 2;
canvas.drawCircle(circleX, circleY, radius, mRingPaint);
// draw progress ring
float sweepAngle = (float) mCurProgress / mMaxProgress * 360; // 繪製當前進度
// 4 個座標點,由於弧須要經過矩形來肯定自身的位置與大小
float leftTop = (float) mRingWidth / 2;
float rightBottom = getWidth() - leftTop;
// 建立肯定弧位置大小的矩形
RectF oval = new RectF(leftTop, leftTop, rightBottom, rightBottom);
// -90 表示在時鐘的 0 點開始繪製,sweepAngle 就是繪製範圍,
// useCenter 爲 false 表示不以扇形繪製
canvas.drawArc(oval, -90, sweepAngle, false, mProgressPaint);
// draw text
// 包含所有文本的最小矩形
Rect bounds = new Rect();
mTextPaint.getTextBounds(mText, 0, mText.length(), bounds);
// x 和 y 決定在 View 的哪一個位置開始繪製,文本的繪製是在矩形的左下角開始的
float x = (float) getWidth() / 2 - (float) bounds.width() / 2;
float y = (float) getWidth() / 2 + (float) bounds.height() / 2;
canvas.drawText(mText, x, y, mTextPaint);
}
/**
* 設置點擊監聽器,與 setOnClickListener 區別在於形參是實現了 OnClickListener 的抽象類,
* 在實現的 onClick(View v) 中設置了被點擊標記位,並須要使用者實現抽象方法。
*
* @param listener 實現了 OnClickListener 的抽象類
*/
public void setOnPressListener(OnPressListener listener) {
setOnClickListener(listener);
}
/**
* 進度條開始滑動
*
* @param countDown 倒計時具體毫秒後中止滑動
*/
public void startSlide(long countDown, SlideCallback callback) {
// 每 mMaxProgress 分之一的進度須要休眠的毫秒數
long sleep = countDown / mMaxProgress;
new Thread(() -> {
// 循環設置當前進度,若是沒有休眠的話是看不到進度滑動的
for (int i = 0; i < mMaxProgress; i++) {
// 若是被點擊或 Context 已銷燬則跳出循環中止滑動,並從新賦值 sIsSkip 爲 false
// 避免因持有 Context 而形成內存泄漏
if (sIsSkip || ((Activity) getContext()).isFinishing()) {
sIsSkip = false;
return;
}
SystemClock.sleep(sleep); // 開始休眠
this.setCurProgress(i + 1); // 設置當前進度,在 onDraw() 中經過這個去繪製進度
this.postInvalidate(); // 通知 View 進行繪製
// 將當前進度回調給調用方,調用方可根據當前進度來實現具體邏輯
// 使用 post()是由於回調在主線程發出後,調用方就不用再去切換回主線程了
this.post(() -> callback.onProgress(mCurProgress, mMaxProgress));
}
}).start();
}
/**
* 初始化自定義屬性
*/
private void init(Context context, AttributeSet attrs) {
if (attrs != null) {
// 這裏屬於 View 的自定義屬性,經過使用者在 layout 文件中寫入的參數進行賦值
// 若是沒有主動賦值則使用 View 的默認參數進行賦值
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RoundProgressBar);
mRingColor = typedArray.getColor(R.styleable.RoundProgressBar_ringColor, RING_COLOR);
mProgressColor = typedArray.getColor(R.styleable.RoundProgressBar_progressColor, PROGRESS_COLOR);
mRingWidth = (int) typedArray.getDimension(R.styleable.RoundProgressBar_ringWidth, RING_WIDTH);
String text = typedArray.getString(R.styleable.RoundProgressBar_text);
mText = text != null ? text : TEXT;
mTextSize = (int) typedArray.getDimension(R.styleable.RoundProgressBar_textSize, TEXT_SIZE);
mTextColor = typedArray.getColor(R.styleable.RoundProgressBar_textColor, TEXT_COLOR);
mMaxProgress = typedArray.getInteger(R.styleable.RoundProgressBar_maxProgress, MAX_PROGRESS);
mCurProgress = typedArray.getInteger(R.styleable.RoundProgressBar_curProgress, CUR_PROGRESS);
typedArray.recycle();
} else {
// 這一段用於使用者經過 Java 代碼直接建立 View 後進行默認參數賦值
mRingColor = RING_COLOR;
mProgressColor = PROGRESS_COLOR;
mRingWidth = RING_WIDTH;
mText = TEXT;
mTextSize = TEXT_SIZE;
mTextColor = TEXT_COLOR;
mMaxProgress = MAX_PROGRESS;
mCurProgress = CUR_PROGRESS;
}
initUtils(); // 初始化工具,如畫筆
}
/**
* 初始化畫筆等工具
*/
private void initUtils() {
mRingPaint = new Paint();
mRingPaint.setAntiAlias(true); // 開啓抗鋸齒
mRingPaint.setStyle(Paint.Style.FILL); // FILL 表示繪製實心,STROKE 表示繪製空心
mRingPaint.setColor(mRingColor);
mProgressPaint = new Paint();
mProgressPaint.setAntiAlias(true);
mProgressPaint.setStyle(Paint.Style.STROKE);
mProgressPaint.setStrokeWidth(mRingWidth);
mProgressPaint.setColor(mProgressColor);
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
mTextPaint.setColor(mTextColor);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setStrokeWidth(0);
}
/**
* 因爲直接繼承 View ,爲了不在使用 wrap_content 時 View 無限大,所以須要從新測量大小
* 這裏沒什麼說的,繼承自 View 的都須要本身測量大小
*/
private int measureSize(int measureSpec) {
int result;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = WIDTH;
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
// 下面爲 getter 和 setter 方法,爲 View 自定義屬性的主動賦值修改與獲取,可動態更改屬性
public int getRingColor() {
return mRingColor;
}
public void setRingColor(int ringColor) {
mRingColor = ringColor;
}
public int getProgressColor() {
return mProgressColor;
}
public void setProgressColor(int progressColor) {
mProgressColor = progressColor;
}
... // 省略餘下的 getter 和 setter 方法
/**
* 自定義點擊監聽器,在實現方法內加入被點擊標記位
*/
public static abstract class OnPressListener implements OnClickListener {
@Override
public void onClick(View v) {
sIsSkip = true;
onPress(view);
}
public abstract void onPress(View view);
}
/**
* 回調滑動進度
*/
public interface SlideCallback {
void onProgress(int curProgress, int maxProgress);
}
}
複製代碼
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RoundProgressBar">
<attr name="ringColor" format="color" />
<attr name="progressColor" format="color" />
<attr name="ringWidth" format="dimension" />
<attr name="text" format="string" />
<attr name="textSize" format="dimension" />
<attr name="textColor" format="color" />
<attr name="maxProgress" format="integer" />
<attr name="curProgress" format="integer" />
</declare-styleable>
</resources>
複製代碼
<com.xxx.xxx.RoundProgressBar
android:id="@+id/round_progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:curProgress="0"
app:maxProgress="100"
app:progressColor="@android:color/holo_red_light"
app:ringColor="@android:color/darker_gray"
app:ringWidth="10dp"
app:text="跳過"
app:textColor="@android:color/white"
app:textSize="12sp"/>
複製代碼
RoundProgressBar roundProgressBar = findViewById(R.id.round_progress_bar);
roundProgressBar.setOnPressListener(new RoundProgressBar.OnPressListener() {
@Override
public void onPress(View view) {
// 因爲我用在開屏頁,有定時跳轉策略,所以點擊後須要移除
handler.removeCallbacksAndMessages(null);
... // 這裏你能夠實現本身的邏輯
}
});
// 加載圖片
ImageLoader.getInstance().loadImage(this, urls, mTarget, new ImageLoader.Listener<Drawable>() {
@Override
public void onSuccess(Drawable drawable) {
// 既然加載到了服務器的圖片,那麼定時器也必須移除
handler.removeCallbacksAndMessages(null);
// 在佈局文件中 RoundProgressBar 的 visibility=gone
// 所以這裏應該 VISIBLE ,緣由很簡單,若是加載不到圖片
// 那麼 RoundProgressBar 也沒有必要顯示出來
roundProgressBar.setVisibility(View.VISIBLE);
// 圖片加載成功,開始滑動進度,這裏是設置以 4000 毫秒的時長來滑動進度的,
// 若是 maxProgress 是 100 ,則每百分之一的進度須要耗時 40 毫秒,
// 滑動進度經過 RoundProgressBar 內的 SlideCallback 進行回調
roundProgressBar.startSlide(4000, new RoundProgressBar.SlideCallback() {
@Override
public void onProgress(int curProgress, int maxProgress) {
... // 這裏根據回調實現邏輯
}
});
}
@Override
public void onFailure(Throwable throwable) {
// 當加載圖片發生異常時回調,可處理也可不處理,根據我的須要
}
});
複製代碼
到這裏,一個全新的控件就實現完成了,過程仍是很簡單的。文章中有寫得不對或者不夠嚴謹的地方,還但願讀者們能提出指正,感謝!服務器