RoundProgressBar 的實現

前言

最近在作 App 的開屏頁,通常都是建立一個 SplashActivity 來展現 Logo 與廣告圖,因此這裏我也不例外,須要展現的圖片能夠是本地固定好的,也能夠是與服務器交互請求獲取到 Url 再進行加載,圖片請求加載很簡單,這裏就很少說了,接下來進入正題。android

想法

原本我只是想實現網易雲音樂那樣的,如圖: canvas

但彷佛領導比較喜歡帶進度的按鈕,如圖:
網易雲音樂的這個很簡單,設置好定時策略,決定延遲多久後跳轉主頁,而後就是那個{ 跳過}按鈕,普通的 Button換個背景就好了,像網易雲音樂那樣的背景,本身用 drawable-shape切一個圓角矩形便可。而有道的這個稍微複雜一點,既要有進度又要能點擊,普通的 ProgressBar應該是實現不了的(由於普通的圓形 ProgressBar是不肯定狀態,不知道這樣說對不對),因此我選擇自定義一個 ProgressBar

思路

做爲開屏頁,那確定只是用來展現的(你也能夠在這裏初始化一些東西傳遞給下一個界面),既然如此那就須要在必定時間內進行跳轉了,這裏我是用handler.postDelayed() 設置延遲,在加載不到圖片的狀況下 2 秒後進行跳轉,若是加載到圖片了則調用removeCallbacksAndMessages()來移除這個延時操做,當進度條走完時再進行跳轉,也能夠直接點擊{跳過}按鈕執行跳轉。話很少說,自定義 View 搞起來。bash

自定義 View - RoundProgressBar

  • Java 層
/**
 * 一個圓形進度條,用於開屏廣告顯示進度
 *
 * @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 層
  1. 自定義的屬性,在 values 文件夾下建立一個 attrs.xml 文件。屬性名最好是對應 Java 層裏面的屬性名,這樣清晰直觀
<?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>
複製代碼
  1. layout 文件中使用,使用 app 命名空間引用自定義的屬性,也能夠不對自定義屬性賦值,有默認值,根據本身須要來修改
<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 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) {
        // 當加載圖片發生異常時回調,可處理也可不處理,根據我的須要
    }
});
複製代碼

效果圖

後記

到這裏,一個全新的控件就實現完成了,過程仍是很簡單的。文章中有寫得不對或者不夠嚴謹的地方,還但願讀者們能提出指正,感謝!服務器

相關文章
相關標籤/搜索