前言
今天接着上一篇來寫關於自定義View方面的東西,我是近期在學習整理這方面的知識點,所以把相關的筆記都放到這個Android自定義View的專欄裏了,方便自己下次忘記的時候能回來翻翻,今天的內容是關於Paint畫筆方面的一個應用,做一個Demo——仿QQ運動步數進度效果。
一、案例效果分析
首先上一張效果圖,大家一起來看一下:
通過上面的這張圖,我們來分析一下,該如何去實現這個效果呢?我們採用從整體到局部的思路來分析哈:
整個效果分爲四個部分,那麼我們就要分四步去繪製它:
- ①、固定不變的大圓弧(color、border width)
- ②、動態變化的小圓弧(color、border width)
- ③、靠上方的提示文字
- ④、中間的步數文字(color、textSize)
二、分步驟繪製
首先重寫onDraw()方法,我們需要在這個方法中進行繪製:
2.1、自定義屬性
在上面我們已經分析過了實現思路,那麼現在我們就來根據這個思路一步一步的去完成這個效果的實戰開發。OK,上一篇我們已經介紹了自定義View的套路步驟,現在依然是按照這個步驟來,先來第一步,自定義屬性配置,在res/values下新建attrs.xml文件,在裏面定義需要的相關屬性:
<!--仿QQ運動步數效果-->
<declare-styleable name="QQStepView">
<attr name="outerColor" format="color" /> <!--外圓弧顏色-->
<attr name="innerColor" format="color" /> <!--內圓弧顏色-->
<attr name="stepBorderWidth" format="dimension" /> <!--圓弧寬度-->
<attr name="stepTextSize" format="dimension" /> <!--步數文字大小-->
<attr name="stepTipSize" format="dimension" /> <!--提示文字大小-->
<attr name="stepTextColor" format="color" /> <!--文字顏色-->
</declare-styleable>
做完這一步之後,別管三七二十一的先在自定義的QQStepView的構造方法中把自定義屬性獲取了:
private int mOurterColor = Color.RED; //默認值
private int mInnerColor = Color.BLUE;
private int mBorderWidth = 20;
private int mStepTextSize = 20;
private int mStepTipSize = 32;
private int mStepTextColor = Color.RED;
public QQStepView(Context context) {
this(context, null);
}
public QQStepView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public QQStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//獲取自定義屬性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QQStepView);
//兩個參數:第一個屬性:attrs中定義的屬性,格式R.styleable.name_attribute,第二個參數:默認值
//比如:<declare-styleable name="QQStepView"> <attr name="outerColor" format="color" /> </declare-styleable>
//那麼這裏的name則爲QQStepView,attribute則爲outerColor
mOurterColor = array.getColor(R.styleable.QQStepView_outerColor, mOurterColor);
mInnerColor = array.getColor(R.styleable.QQStepView_innerColor, mInnerColor);
mBorderWidth = array.getDimensionPixelSize(R.styleable.QQStepView_stepBorderWidth, mBorderWidth);
mStepTextSize = array.getDimensionPixelSize(R.styleable.QQStepView_stepTextSize, TransferUtil.sp2px(mStepTextSize));
mStepTipSize = array.getDimensionPixelSize(R.styleable.QQStepView_stepTipSize, TransferUtil.sp2px(mStepTipSize));
mStepTextColor = array.getColor(R.styleable.QQStepView_stepTextColor, mStepTextColor);
array.recycle();
}
2.2、在佈局中引用
首先我們在頁面頂部標籤中先定義命名空間:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
然後在佈局中通過 全類名 引用我們的自定義控件,通過命名空間app引用自定義屬性:
<com.jarchie.customview.view.QQStepView
android:id="@+id/mStepView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
app:innerColor="@color/innerColor"
app:outerColor="@color/colorPrimary"
app:stepBorderWidth="15dp"
app:stepTextColor="@color/colorPrimary"
app:stepTextSize="34sp"
app:stepTipSize="40sp" />
2.3、初始化畫筆
在文章的一開始我們分析了這個完整的效果需要分成四塊去繪製,所以這裏定義四個畫筆,每個畫筆去繪製它自己那部分,職責分工先確定了,初始化的工作我們也是放在了構造方法中去完成:
//分別定義四個畫筆:外圓畫筆、內圓畫筆、提示文字畫筆、步數文字畫筆
private Paint mOurterPaint, mInnerPaint, mTextPaint,mStepPaint;
//在構造方法中初始化定義的畫筆,這裏省略了構造方法,直接貼上初始化部分的代碼
//設置外圓畫筆
mOurterPaint = new Paint();
mOurterPaint.setAntiAlias(true); //設置抗鋸齒
mOurterPaint.setStrokeWidth(mBorderWidth);
mOurterPaint.setColor(mOurterColor);
mOurterPaint.setStrokeCap(Paint.Cap.ROUND); //設置下方爲圓弧形
mOurterPaint.setStyle(Paint.Style.STROKE); //畫筆空心
//設置內圓畫筆
mInnerPaint = new Paint();
mInnerPaint.setAntiAlias(true);
mInnerPaint.setStrokeWidth(mBorderWidth);
mInnerPaint.setColor(mInnerColor);
mInnerPaint.setStrokeCap(Paint.Cap.ROUND); //設置下方爲圓弧形
mInnerPaint.setStyle(Paint.Style.STROKE); //畫筆空心
//設置提示文字畫筆
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
mTextPaint.setColor(mStepTextColor);
mTextPaint.setTextSize(mStepTipSize);
//設置步數畫筆
mStepPaint = new Paint();
mStepPaint.setAntiAlias(true);
mStepPaint.setColor(mStepTextColor);
mStepPaint.setTextSize(mStepTextSize);
2.4、測量寬高
初始化的工作完成之後,接下來需要做的就是測量了,因爲在繪製之前我們肯定是需要先測量大小,確定大小之後畫筆才能知道該去畫多大的啊。這裏爲了好看,我們給它確定大小的時候確定它爲正方形,我們在使用自定義控件的時候給的大小可能是wrap_content,也可能是某個具體的值,並且寬高的值也可能是不一樣的,所以這裏我們規定寬高值都取寬和高中值較小的:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//調用者在佈局文件中可能是wrap_content,可能是寬度高度不一致
//獲取模式 AT_MOST 40dp
//寬度高度不一致取最小值,確保是個正方形
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width > height ? height : width, width > height ? height : width);
}
2.5、分步驟繪製
首先繪製外面的大圓弧:
關於繪製相關api詳見:https://blog.csdn.net/JArchie520/article/details/78199580
首先來看上面這張圖,這裏的座標系和我們平時在數學中學的兩維座標系不同,android中每個View自己有一個座標系,原點是View左上角的那個點,水平方向爲X軸,向右爲正向左爲負,豎直方向爲Y軸,向下爲正向上爲負,然後我們使用
drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint)方法來繪製弧形或扇形,通過查看源碼我們可以發現,它底層是通過一個橢圓來描述弧形的,其中參數left、top、right、bottom是所在的橢圓的邊界座標,startAngle是弧形的起始角度,X軸的正向爲0度的位置,順時針爲正逆時針爲負,sweepAngle是弧形範圍角度,useCenter表示是否連接到圓心,爲true表示連接,就是扇形,爲false表示不連接,就是弧形。所以我們通過RectF來確定圓弧的外輪廓的位置,然後調用api繪製,這裏起始角度爲135°,掃過的角度爲270°:
@SuppressLint("DrawAllocation")
RectF rectF = new RectF(mBorderWidth / 2, mBorderWidth / 2, getWidth() - mBorderWidth / 2, getHeight() - mBorderWidth / 2);
canvas.drawArc(rectF, 135, 270, false, mOurterPaint);
然後繪製內圓弧:
由效果圖可以看出,內圓弧掃過的角度是由當前步數跟最大步數之間通過折算得到的一個比例值,所以我們先定義最大步數和當前步數兩個變量值:
private int mStepMax; //總步數
private int mCurrentStep; //當前步數
接着根據百分比乘以總的度數270°就得到了當前步數掃過的角度:
//畫內圓弧:不能寫死,是根據步數計算出來的百分比
if (mStepMax == 0) return;
float sweepAngle = (float) mCurrentStep / mStepMax;
canvas.drawArc(rectF, 135, sweepAngle * 270, false, mInnerPaint);
然後繪製提示文字:
關於文本繪製及基線計算詳見:https://blog.csdn.net/JArchie520/article/details/105610884
這裏的文本就是固定的“今日步數”,我們這裏取整個View的1/3位置顯示這幾個文字,直接調用drawText()繪製:
//畫文字
String tipText = "今日步數";
@SuppressLint("DrawAllocation")
Rect tipBounds = new Rect();
mTextPaint.getTextBounds(tipText, 0, tipText.length(), tipBounds);
int start = getWidth() / 2 - tipBounds.width() / 2;
//基線 baseLine
Paint.FontMetricsInt metricsInt = mTextPaint.getFontMetricsInt();
int dyH = (metricsInt.bottom - metricsInt.top) / 3 - metricsInt.bottom;
int baseline = getHeight() / 3 + dyH;
canvas.drawText(tipText, start, baseline, mTextPaint);
然後繪製步數:
因爲也是文本的繪製,和上方的提示文字的繪製方法是一樣的,這裏我們選擇整個View的1/2位置繪製:
//畫步數
String stepText = String.valueOf(mCurrentStep);
@SuppressLint("DrawAllocation")
Rect textBounds = new Rect();
mStepPaint.getTextBounds(stepText, 0, stepText.length(), textBounds);
int dx = getWidth() / 2 - textBounds.width() / 2;
//基線 baseLine
Paint.FontMetricsInt fontMetricsInt = mStepPaint.getFontMetricsInt();
int dy = (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom;
int baseLine = getHeight() / 2 + dy;
canvas.drawText(stepText, dx, baseLine, mStepPaint);
到這裏我們的繪製整個View的操作基本上就完成了,剩下的工作就是調用這個View顯示了,在調用之前我們還需要做一步操作,我們需要設置最大步數和當前步數的值,大家考慮一下在哪裏設置呢?有的人可能會直接寫在了自定義View中,但是這樣有個缺點,這樣寫就限制死了,如果有多個地方調用,並且顯示的步數值都不一樣,那麼就要每次都改動這個View,導致程序的可擴展性很低,所以我們定義兩個方法,將設置值的操作拋出去,哪裏調用哪裏設置,由於設置當前步數時它是一個不斷變化的過程,所以我們在方法內部還調用了invalidate()方法,不斷刷新繪製:
//設置步數最大值
public synchronized void setStepMax(int stepMax) {
this.mStepMax = stepMax;
}
//設置當前步數
public synchronized void setCurrentStep(int currentStep) {
this.mCurrentStep = currentStep;
//不斷繪製,不斷調用onDraw
invalidate();
}
三、調用顯示
咱們新建了一個頁面,在這裏做調用操作,首先設置最大步數我這裏設置了18000,接着使用屬性動畫,因爲頁面中大家也看到了是一個動畫效果,通過屬性動畫將內圓弧和文字做成一個不斷變化的效果,這裏還給屬性動畫設置了插值器,這個插值器的效果是變化的過程是先快後慢的一個效果,調用的部分就是這麼簡單:
/**
* 作者:created by Jarchie
* 時間:2020/4/22 17:02:49
* 郵箱:[email protected]
* 說明:QQ運動步數進度效果
*/
public class QQStepActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ActivityQqstepLayoutBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_qqstep_layout);
CommonFinishViewModel viewModel = new CommonFinishViewModel(this);
binding.setViewModel(viewModel);
//屬性動畫 後面會說
binding.mStepView.setStepMax(18000);
ValueAnimator valueAnimator = ObjectAnimator.ofFloat(0, 7890);
valueAnimator.setDuration(1200);
//設置插值器,先快後慢
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float currentStep = (float) animation.getAnimatedValue();
binding.mStepView.setCurrentStep((int) currentStep);
}
});
valueAnimator.start();
}
}
到這裏就寫完了,以上就是整個仿QQ運動步數進度效果的實現過程,代碼相對來說並不複雜,因爲我這是一個自定義View的系列案例,所以代碼也放在了Github上方便查看!