安卓自定義錶盤
前情
因爲最近公司項目在做飲水打卡的模塊,所以需要有一個錶盤去顯示飲水進度。
設計稿以及需求理解
- 成品圖
gayhub地址
-
分析
根據需求,我們可以把錶盤分爲幾個模塊:
- 最外層帶陰影的模塊
- 倒數第二層帶進度條的模塊
- 位於倒數第二層進度條上的打卡位置
- 裏面的刻度模塊
- 中心的圖片模塊
套用我弟的名言:猶豫不決總是夢,開幹!
開幹
初具模型
玩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走查的時候被設計吐槽了並且打回修改,比如上圖的外圈陰影模塊就是被設計拎着改了一箇中午才改出來的...
一般我們應對陰影會給出幾種方案:
- cardView
- .9
- drawable
- 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上面會出現,也試了很多方法:
- 原bitmap大小
- 動態去修改大小
- ...
撲街...
後來發現也和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