Android 自定義一個打卡錶盤

安卓自定義錶盤

前情

因爲最近公司項目在做飲水打卡的模塊,所以需要有一個錶盤去顯示飲水進度。

設計稿以及需求理解

  1. 成品圖



gayhub地址

  1. 分析

    根據需求,我們可以把錶盤分爲幾個模塊:

    1. 最外層帶陰影的模塊
    2. 倒數第二層帶進度條的模塊
    3. 位於倒數第二層進度條上的打卡位置
    4. 裏面的刻度模塊
    5. 中心的圖片模塊

套用我弟的名言:猶豫不決總是夢,開幹!

開幹

初具模型

玩View我喜歡先在onDraw那裏畫個十字座標系,大概是因爲腦部能力有限,有個座標系更方便想象。


    /**
     * 畫輔助座標系
     *
     * @param canvas
     */
    private void drawSystem(Canvas canvas) {
        canvas.drawLine(-mDx, 0, mDx, 0, mPaintForComment);
        canvas.drawLine(0, -mDy, 0, mDy, mPaintForComment);
    }

按照我們之前的分析,先把各個模塊劃分出來,依次實現即可:

 @Override
    protected void onDraw(Canvas canvas) {
        canvas.setDrawFilter(pfd);
        super.onDraw(canvas);
        canvas.translate(mDx, mDy);
//        drawSystem(canvas);
        drawShader(canvas);
        drawCenterImg(canvas);
        drawCircle(canvas);
        drawNumbers(canvas);
        drawProgress(canvas);
        drawScaleImg(canvas);
    }

注意這裏我把圓心挪到了中間點,這樣比較方便,即座標系爲:


陰影模塊

給view添加陰影是最常見的需求,很多時候圖省事就是一個cardView包上去,然而結果肯定是UI走查的時候被設計吐槽了並且打回修改,比如上圖的外圈陰影模塊就是被設計拎着改了一箇中午才改出來的...

一般我們應對陰影會給出幾種方案:

  1. cardView
  2. .9
  3. drawable
  4. view加

這裏用了ShadowLayer來做陰影。

public void setShadowLayer(float radius, float dx, float dy, int color)  
  • radius:模糊半徑,radius越大越模糊,越小越清晰,但是如果radius設置爲0,則陰影消失不見
  • dx:陰影的橫向偏移距離,正值向右偏移,負值向左偏移
  • dy:陰影的縱向偏移距離,正值向下偏移,負值向上偏移
  • color: 繪製陰影的畫筆顏色,即陰影的顏色(對圖片陰影無效)

實操代碼爲:

  /**
     * 畫陰影
     *
     * @param canvas
     */
    private void drawShader(Canvas canvas) {
       
        mPaintForShader.setShadowLayer(20, 1, 1, Color.parseColor("#3363BAFF"));
        mPaintForShader.setAntiAlias(true);
        mPaintForShader.setColor(Color.WHITE);
        mPaintForShader.setStyle(Paint.Style.FILL);
        canvas.drawCircle(0, 0, mRadius + mDefOutSizeCircleWidth, mPaintForShader);
    }

畫基礎的圓形

這個沒什麼好說的,就是一個圓形

    /**
     * 畫基礎的圓形 也就是默認的沒打卡的點
     *
     * @param canvas
     */
    private void drawCircle(Canvas canvas) {
        mPaintForCircle.setAntiAlias(true);
        mPaintForCircle.setStrokeWidth(mWidthForCircle);
        mPaintForCircle.setColor(mColorForCircle);
        mPaintForCircle.setStyle(Paint.Style.STROKE);
        canvas.drawCircle(0, 0, mRadius, mPaintForCircle);
    }

畫刻度

因爲我們目前是推薦一日八杯水,所以刻度值爲1~8,畫這種隨着弧度而弧度的字,推薦是讓canvas 進行translate配合rotate,

 /**
     * 繪製進度刻度
     *
     * @param canvas
     */
    private void drawNumbers(Canvas canvas) {
        int singleAngle = 360 / mPunchList.size();
        for (int i = 0; i < mScaleMsgList.size(); i++) {
            mPaintForText.setTextSize(mScaleFontSize);
            String text = mScaleMsgList.get(i);
            Rect textBound = new Rect();
            mPaintForText.getTextBounds(text, 0, text.length(), textBound);
            canvas.save();
            canvas.translate(0, -mRadius + dip2px(getContext(), 2) + mPadding + ((textBound.bottom - textBound.top) >> 1));
            canvas.rotate(-singleAngle * i);
            if (i == mTargetIndex) {
                mPaintForCircle.setColor(mColorForText);
                mPaintForText.setColor(mColorForTextWithTarget);
                mPaintForCircle.setStyle(Paint.Style.FILL);
                mPaintForCircle.setAntiAlias(true);
                canvas.drawCircle(0, 0, mDefNumberCircleRadius * 0.95f, mPaintForCircle);
            } else {
                mPaintForText.setColor(mColorForText);
            }
            canvas.drawText(text, ((float) (textBound.right + textBound.left) / -2), ((float) -(textBound.bottom + textBound.top) / 2), mPaintForText);
            canvas.restore();
            canvas.rotate(singleAngle);
        }
    }

畫進度條

雖然不說看不太出來,但是其實進度條是一個漸變色的哦...

    //進度條漸變色
    private int mColorProgressStart = Color.parseColor("#97e0fb");
    private int mColorProgressEnd = Color.parseColor("#97f6e5");

漸變色我一般用LinearGradient處理:

LinearGradient(float x0, float y0, float x1, float y1, int colors[], float positions[], TileMode tile)
  • 第一個參數爲線性起點的x座標
  • 第二個參數爲線性起點的y座標
  • 第三個參數爲線性終點的x座標
  • 第四個參數爲線性終點的y座標
  • 第五個參數爲實現漸變效果的顏色的組合
  • 第六個參數爲前面的顏色組合中的各顏色在漸變中佔據的位置(比重),如果爲空,則表示上述顏色的集合在漸變中均勻出現
  • 第七個參數爲渲染器平鋪的模式,一共有三種:
    • -CLAMP 邊緣拉伸
    • -REPEAT 在水平和垂直兩個方向上重複,相鄰圖像沒有間隙
    • -MIRROR 以鏡像的方式在水平和垂直兩個方向上重複,相鄰圖像有間隙 (我不喜歡這個,密恐患者路過)

數據源處理

這裏插一句,因爲後臺的數據結構問題,數據源我打算用map來做處理,即:

Map<Integer,Boolean>

key作爲打卡點,value作爲是否飲水打卡的標誌。

這裏強烈推薦 SparseBooleanArray:

public class SparseBooleanArray implements Cloneable {
    ...
}

真香!!

具體實現

 /**
     * 畫進度條
     *
     * @param canvas
     */
    private void drawProgress(Canvas canvas) {
        int[] colors = {mColorProgressStart, mColorProgressEnd};
        LinearGradient linearGradient = new LinearGradient(-mStartPointX, mStartPointY, mEndPointX, mEndPointY,
                colors,
                null, Shader.TileMode.REPEAT);
        mPaintForComment.setAntiAlias(true);
        mPaintForComment.setStrokeWidth(mWidthForCircle);
        mPaintForComment.setStyle(Paint.Style.STROKE);
        mPaintForComment.setStrokeCap(Paint.Cap.ROUND);

        RectF f = new RectF(-mRadius, -mRadius, mRadius, mRadius);
        int angle = 360 / mPunchList.size();
        for (int i = 1; i <= mPunchList.size(); i++) {
            if (mPunchList.get(i)) {
                mPaintForComment.setShader(linearGradient);
                mPaintForComment.setStrokeCap(Paint.Cap.ROUND);
                canvas.drawArc(f, (i - 3) * angle, angle, false, mPaintForComment);
            }
        }
    }

畫進度打卡點

從成品圖可以看到,打卡點是位於進度條上的,要拿到它的點的位置,就需要一點三角函數的計算

 /**
     * 畫進度條到了哪天打卡
     *
     * @param canvas
     */
    private void drawScaleImg(Canvas canvas) {
        canvas.save();
        Bitmap scaleImg = BitmapFactory.decodeResource(getResources(), R.mipmap.icon_tick);
        int width = (int) (Math.min(scaleImg.getWidth(), mDefTargetImgSize) * 0.90);
        int height = (int) (Math.min(scaleImg.getHeight(), mDefTargetImgSize) * 0.90);
        mTargetBitmap = Bitmap.createScaledBitmap(scaleImg, width, height, true);
        scaleImg.recycle();
        int allNumbers = mPunchList.size();
        int single = (360 / allNumbers);
        if (mDefSignIndex >= 0 && mDefSignIndex <= 7 && mTargetBitmap != null) {
            double radian = 2 * PI / 360 * (360 - single * (1 - mDefSignIndex));
            int xD = (int) (Math.cos(radian) * mRadius);
            int yD = (int) (Math.sin(radian) * mRadius);
            Rect rect = new Rect(xD - width / 2, yD - height / 2, xD + width / 2, yD + height / 2);
            mPaintForComment.setAntiAlias(true);
            canvas.drawBitmap(mTargetBitmap, null, rect, mPaintForComment);
        }
        canvas.restore();
    }

怎麼說呢,我只想對我的數學老師說我錯了,我後悔了。

畫居中的水杯圖

   /**
     * 畫居中的圖片
     *
     * @param canvas
     */
    private void drawCenterImg(Canvas canvas) {
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), mResIdForCup);
        int width = (bitmap.getWidth());
        int height = (bitmap.getHeight());
        int realW = (int) ((width * mDx / height) * 0.9);
        Rect rect = new Rect(-realW * 2 / 3, (int) (-mRadius * 2 / 3), realW * 2 / 3, (int) (mRadius * 2 / 3));
        canvas.drawBitmap(bitmap, null, rect, mPaintForComment);
    }

定義屬性,style

這一個模塊的沒什麼好說的,

 private void initUserAttrs(AttributeSet attrs) {
        TypedArray array = null;
        try {
            array = getContext().obtainStyledAttributes(attrs, R.styleable.MeterView);
            mPadding = array.getDimension(R.styleable.MeterView_def_padding, dip2px(getContext(), 10));
            mScaleFontSize = array.getDimension(R.styleable.MeterView_def_font_size, dip2px(getContext(), 8));
            mColorForText = array.getColor(R.styleable.MeterView_def_font_color, Color.parseColor("#64BAFF"));
            mColorForCircle = array.getColor(R.styleable.MeterView_def_circle_color, Color.parseColor("#F9F9F9"));
            mWidthForCircle = array.getDimension(R.styleable.MeterView_def_circle_width, 30);
            mColorProgressStart = array.getColor(R.styleable.MeterView_def_progress_gradient_start, Color.parseColor("#97e0fb"));
            mColorProgressEnd = array.getColor(R.styleable.MeterView_def_progress_gradient_end, Color.parseColor("#97f6e5"));
            mDefNumberCircleRadius = array.getDimension(R.styleable.MeterView_def_number_circle_radius, dip2px(getContext(), 10));
            mDefTargetImgSize = array.getDimension(R.styleable.MeterView_def_target_img_size, dip2px(getContext(), 20));
        } catch (Exception e) {
            mScaleFontSize = dip2px(getContext(), 8);
            mPadding = dip2px(getContext(), 10);
            e.printStackTrace();
        }
        if (array != null) {
            array.recycle();
        }
    }

踩坑&疑問

因爲一直都是混日子...哎,不知道咋說,工作也是挺久了,怎麼總結呢?

勤奮得感動了自己,然而p用沒有

適配問題

在項目裏面的一個fragment裏面會出現打卡的圖片邊緣鋸齒問題,一開始懷疑是我create出來的bitmap被拉伸了or像素太低等原因,且只有在紅米note4上面會出現,也試了很多方法:

  1. 原bitmap大小
  2. 動態去修改大小
  3. ...

撲街...

後來發現也和fragment所放置的viewpager添加了PageTransformer有關:

public class CardTransformer implements ViewPager.PageTransformer {
    private static final float MAX_SCALE = 0.95f;
    private static final float MIN_SCALE = 0.80f;//0.85f
    private onScaleChange mOnScaleChange;

    public void setOnScaleChange(onScaleChange onScaleChange) {
        mOnScaleChange = onScaleChange;
    }

    public CardTransformer() {
        float result = MIN_SCALE + (MAX_SCALE - MIN_SCALE);
        Log.d("lht", "CardTransformer: " + result);
    }

    @Override
    public void transformPage(@NotNull View page, float position) {
        if (position <= 1) {
            //   1.2f + (1-1)*(1.2-1.0)
            float scaleFactor = MIN_SCALE + (1 - Math.abs(position)) * (MAX_SCALE - MIN_SCALE);
            Log.d("lht", "transformPage: " + scaleFactor);
            page.setScaleX(scaleFactor);  //縮放效果

            if (position > 0) {
                page.setTranslationX(-scaleFactor * 2);
            } else if (position < 0) {
                page.setTranslationX(scaleFactor * 2);
            }
            page.setScaleY(scaleFactor);

            if (mOnScaleChange != null) {
                mOnScaleChange.onChange(scaleFactor);
            }
        } else {
            page.setScaleX(MIN_SCALE);
            page.setScaleY(MIN_SCALE);
//            if (mOnScaleChange != null) {
//                mOnScaleChange.onChange(MIN_SCALE);
//            }
        }
    }

    public interface onScaleChange {
        void onChange(float scale);
    }
}

懷疑是在fragment被拉伸了,因爲這個view的屬性爲:

  <com.xxx.xxx.view.meter.MeterView
            android:id="@+id/tv_plan_meter"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:def_number_circle_radius="@dimen/margin_6"
            app:layout_constraintBottom_toTopOf="@id/tv_plan_conn"
            app:layout_constraintDimensionRatio="w,1:1"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_plan_name"
            app:layout_constraintWidth_percent="0.83"
            />

最後做了一個無奈的辦法:

        constraintLayout.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) mMeterView.getLayoutParams();
                layoutParams.width = (int) (constraintLayout.getWidth() * 0.83);
                layoutParams.height = (int) (constraintLayout.getWidth() * 0.83);
                mMeterView.setLayoutParams(layoutParams);
                return true;
            }
        });

canvan的clipXX方法:

其實這個我之前也是一直用的:之前的文章

不過一直都只是覺得方便、畫圖好用而已...

最近在看優化才知道這個東西用得好也可以用來降低過度繪製問題,挺不錯的。

總結

很多不足,還是要補啊... 互勉!!

and

飲茶+聽歌+coding=真的好舒服。

and

這首歌賊好聽

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章