分享一個最近實現的一個效果,主要是用來顯示分數。分數的範圍是0~100,沒有小數。java
在你閱讀此文以前最好先了解自定義View的步驟,好比onMeasure,onLayout,onDraw等等。這類的文章有不少,我這裏再也不一一贅述了。android
一.首先建好Activity,和Activity的佈局:canvas
package com.greendami.gdm
import android.app.Activity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bt_0.setOnClickListener { pp.setProgress("0",0f,true) }
bt_100.setOnClickListener { pp.setProgress("100",100f,true) }
}
}
複製代碼
佈局文件:bash
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:background="@color/colorPrimary">
<LinearLayout
android:layout_marginBottom="100dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<Button
android:id="@+id/bt_0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0" />
<Button
android:id="@+id/bt_100"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="100" />
</LinearLayout>
<com.greendami.gdm.PPCircleProgressView
android:id="@+id/pp"
android:layout_width="200dp"
android:layout_height="200dp" />
</LinearLayout>
複製代碼
而後是一個工具類,主要是用於dp轉px:app
package com.greendami.gdm;
import android.content.Context;
/**
* Created by hsy on 2016/4/8.
*/
public class DPUnitUtil {
/**
* 將px值轉換爲dip或dp值,保證尺寸大小不變
*
* @param pxValue (DisplayMetrics類中屬性density)
* @return
*/
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
}
複製代碼
二.建一個PPCircleProgressView類:ide
自定義的View就叫作PPCircleProgressView。 而後建一個類PPCircleProgressView,繼承View類。工具
package com.greendami.gdm;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
/** * 圓形進度條 * Created by GreendaMi on 2017/3/1. */
public class PPCircleProgressView extends View {
private float progress = 0; //顯示的進度
private String strprogress = "100"; //顯示的進度
private int mLayoutSize = 100;//整個控件的尺寸(方形)
public int mColor;//主要顏色
public int mColorBackground;
Context mContext;
private float now = 0; //當前的進度
public PPCircleProgressView(Context context) {
super(context);
mContext = context;
}
public PPCircleProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
mColor = context.getResources().getColor(R.color.yellow);
mColorBackground = context.getResources().getColor(R.color.colorPrimary);
}
public PPCircleProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
}
}
複製代碼
由於我只用在主xml中,因此要實現帶2個參數的構造方法,在這個方法中取了2個顏色。通常的作法是取style文件,可是我偷懶一下,直接取的color文件中的顏色。佈局
到此準備工做結束。post
三.測量寬高動畫
這是一個方形的View,我偷懶,就把方形定死了,直接在xml給定dp值,設定寬高。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
mLayoutSize = Math.min(widthSpecSize, heightSpecSize);
if (mLayoutSize == 0) {
mLayoutSize = Math.max(widthSpecSize, heightSpecSize);
}
setMeasuredDimension(mLayoutSize, mLayoutSize);
}
複製代碼
三.繪畫
在onDraw方法中繪畫。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(0x66d4b801);
paint.setStyle(Paint.Style.FILL); //設置空心
}
複製代碼
首先是初始化畫筆,固然我知道這直接初始化不太好,通常都是在某處初始化一次,而後調用paint的reset()方法重置。
第一筆就是最外面的一個圓線,顏色是半透明的黃
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(0x66d4b801);//透明的黃色
paint.setStyle(Paint.Style.FILL); //設置填充
//畫半透明黃線
int centre = getWidth() / 2; //獲取圓心的x座標
float radius = centre * 0.96f; //圓環的半徑
canvas.drawCircle(centre, centre, radius, paint); //畫出圓環
.
.
.
.
}
複製代碼
效果是這樣的:
這個圓的半徑是寬的一半,可是因爲那個小水滴的底部會在這個圓的外側,因此這個圓不能夠佔滿整個View,因此 centre * 0.96f,縮小了這個半徑。
接着再畫一個紅色的圓,把半徑減少1,畫在上面那個圓的中心,這樣就是一個圓線了。
//接上面在onDraw中
paint.setColor(mColorBackground);
canvas.drawCircle(centre, centre, radius - DPUnitUtil.dip2px(mContext, 1f), paint); //畫出圓環
複製代碼
效果是這樣的:
接着畫中間一段一段的感受的部分,這裏有一種被‘點亮’的感受。背景‘未點亮’的是半透明的黃色,點亮就是正常的黃色。‘未點亮’是一個360度的扇形,‘點亮’的是角度會變化的扇形。先畫‘未點亮’的部分。
float gap = DPUnitUtil.dip2px(mContext, 14);
RectF rectF = new RectF(gap, gap, mLayoutSize - gap, mLayoutSize - gap);//找出扇形所在的矩形,距離View的邊框上下左右各縮14dp。
paint.setColor(0x44d4b801);
canvas.drawArc(rectF, 0, 360, true, paint);
複製代碼
效果以下:
//15度一個格子,防止佔半個格子
int endR = (int) (360 * (now / 100) / 15) * 15;
paint.setColor(mColor);
canvas.drawArc(rectF, -90, endR, true, paint);
複製代碼
endR是根據當前顯示的分數計算的扇形的結束角度,這裏會根據15°進行四捨五入。開始角度是-90°,扇形是從12點鐘方向開始。這裏的now就是當前的分數,由於是有動畫的,因此now的值會變化,具體是如何變化的後面說,這裏只是根據now值畫扇形。
接着用一個實心的紅色圓把這個扇形的內部‘蓋住’。這個圓的半徑再一次的縮小。這裏乘了一個0.83
//畫紅圓
paint.setColor(mColorBackground); paint.setStyle(Paint.Style.FILL); //設置空心
radius = radius * 0.83f; //圓環的半徑
canvas.drawCircle(centre, centre, radius, paint); //畫出圓環
複製代碼
效果是這樣的:
而後就是把這個比較寬的圓環切成一段一段的。造成‘斷開’的感受。我用的就是比較寬的紅線,每旋轉15°畫一條。
paint.setStrokeWidth(DPUnitUtil.dip2px(mContext, 2));
for (int r = 0; r < 360; r = r + 15) {
canvas.drawLine(centre + (float) ((centre - gap) * Math.sin(Math.toRadians(r))),
centre - (float) ((centre - gap) * Math.cos(Math.toRadians(r))),
centre - (float) ((centre - gap) * Math.sin(Math.toRadians(r))),
centre + (float) ((centre - gap) * Math.cos(Math.toRadians(r))), paint);
}
複製代碼
爲了方便看,我把線設置成的白色,是這樣的:
實際設成紅色,是這樣的:
而後畫內圈的一個淺淺的圓環,這裏的方法和外圈的畫法同樣:
paint.setColor(0x44d4b801);
canvas.drawCircle(centre, centre, radius - DPUnitUtil.dip2px(mContext, 2f), paint); //畫出圓環
paint.setColor(mColorBackground);
canvas.drawCircle(centre, centre, radius - DPUnitUtil.dip2px(mContext, 2.5f), paint); //畫出圓環
複製代碼
效果以下,好像不太明顯:
接下來是畫上面的文字,若是文字是空白的,會畫兩條橫線:
//到此,背景繪製完畢
String per = (int) now + "";
//寫百分比
if ("".equals(strprogress)) {
paint.setColor(mColor);
paint.setStrokeWidth(DPUnitUtil.dip2px(mContext, 2));
canvas.drawLine(centre * 0.77f, centre, centre * 0.95f, centre, paint);
canvas.drawLine(centre * 1.05f, centre, centre * 1.23f, centre, paint);
} else {
paint.setColor(mColor);
paint.setTextSize(mLayoutSize / 4f);//控制文字大小
Paint paint2 = new Paint();
paint2.setAntiAlias(true);
paint2.setTextSize(mLayoutSize / 12);//控制文字大小
paint2.setColor(mColor);
canvas.drawText(per,
centre - 0.5f * (paint.measureText(per)),
centre - 0.5f * (paint.ascent() + paint.descent()),
paint);
canvas.drawText("分",
centre + 0.5f * (paint.measureText((int) now + "") + paint2.measureText("分")),
centre - 0.05f * (paint.ascent() + paint.descent()),
paint2);
}
複製代碼
文字的大小會根據控件的尺寸進行計算。而後就是隨便測量了一下文字的長度,計算文字的位置。上面這些數字後是目測隨便寫的。數字是根據now來變化的。
接下來畫最外面的小水滴,小水滴的位置是和扇形的endR一致。 先畫一個小球:
centre = getWidth() / 2;
canvas.drawCircle(centre + (float) ((centre * 0.95f) * Math.sin(Math.toRadians(endR))),
centre - (float) ((centre * 0.95f) * Math.cos(Math.toRadians(endR))), centre * 0.04f, paint);
複製代碼
小球的圓心是根據endR和最外面的圓環的半徑計算的,小球的半徑就是在外面圓環到View邊的距離(1-0.96)。
Path p = new Path();
p.moveTo(centre + (float) ((centre * 0.86f) * Math.sin(Math.toRadians(endR))),
centre - (float) ((centre * 0.86f) * Math.cos(Math.toRadians(endR))));//頂點
p.lineTo(centre + (float) ((centre * 0.94f) * Math.sin(Math.toRadians(endR + 2.5))),
centre - (float) ((centre * 0.94f) * Math.cos(Math.toRadians(endR + 2.5))));
p.lineTo(centre + (float) ((centre * 0.94f) * Math.sin(Math.toRadians(endR - 2.5))),
centre - (float) ((centre * 0.94f) * Math.cos(Math.toRadians(endR - 2.5))));
p.close();
canvas.drawPath(p, paint);
複製代碼
效果以下:
最後動起來:
if (now < progress - 1) {
now = now + 1;
postInvalidate();
} else if (now < progress) {
now = (int) progress;
postInvalidate();
}
複製代碼
now是當前動畫的顯示數值,progress是最終的顯示數值,若是now < progress - 1則調用postInvalidate()重繪。帶刺onDraw方法結束。
最後加上外部的調用設設值:
/**
* 外部回調
*
* @param strprogress 顯示調進度文字,若是是"",或者null了,則顯示兩條橫線
* @param progress 進度條調進度
* @param isAnim 進度條是否須要動畫
*/
public void setProgress(String strprogress, float progress, boolean isAnim) {
if (strprogress == null) {
this.strprogress = "";
} else {
this.strprogress = strprogress;
}
this.now = 0;
this.progress = progress;
if (!isAnim) {
now = progress;
}
postInvalidate();
}
複製代碼
package com.greendami.gdm;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
/**
* 圓形進度條
* Created by GreendaMi on 2017/3/1.
*/
public class PPCircleProgressView extends View {
private float progress = 0; //顯示的進度
private String strprogress = "100"; //顯示的進度
private int mLayoutSize = 100;//整個控件的尺寸(方形)
public int mColor;//主要顏色
public int mColorBackground;
Context mContext;
private float now = 0; //當前的進度
public PPCircleProgressView(Context context) {
super(context);
mContext = context;
}
public PPCircleProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
mColor = context.getResources().getColor(R.color.yellow);
mColorBackground = context.getResources().getColor(R.color.colorPrimary);
}
public PPCircleProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
mLayoutSize = Math.min(widthSpecSize, heightSpecSize);
if (mLayoutSize == 0) {
mLayoutSize = Math.max(widthSpecSize, heightSpecSize);
}
setMeasuredDimension(mLayoutSize, mLayoutSize);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(0x66d4b801);
paint.setStyle(Paint.Style.FILL); //設置空心
//畫灰線
int centre = getWidth() / 2; //獲取圓心的x座標
float radius = centre * 0.96f; //圓環的半徑
canvas.drawCircle(centre, centre, radius, paint); //畫出圓環
paint.setColor(mColorBackground);
canvas.drawCircle(centre, centre, radius - DPUnitUtil.dip2px(mContext, 1f), paint); //畫出圓環
float gap = DPUnitUtil.dip2px(mContext, 14);
RectF rectF = new RectF(gap, gap, mLayoutSize - gap, mLayoutSize - gap);
//15度一個格子,防止佔半個格子
int endR = (int) (360 * (now / 100) / 15) * 15;
paint.setColor(0x44d4b801);
canvas.drawArc(rectF, 0, 360, true, paint);
paint.setColor(mColor);
canvas.drawArc(rectF, -90, endR, true, paint);
//畫紅圓
paint.setColor(mColorBackground);
paint.setStyle(Paint.Style.FILL); //設置空心
radius = radius * 0.83f; //圓環的半徑
canvas.drawCircle(centre, centre, radius, paint); //畫出圓環
//畫線,許多的線,15度畫一條,線的寬度是2dp
paint.setStrokeWidth(DPUnitUtil.dip2px(mContext, 2));
for (int r = 0; r < 360; r = r + 15) {
canvas.drawLine(centre + (float) ((centre - gap) * Math.sin(Math.toRadians(r))),
centre - (float) ((centre - gap) * Math.cos(Math.toRadians(r))),
centre - (float) ((centre - gap) * Math.sin(Math.toRadians(r))),
centre + (float) ((centre - gap) * Math.cos(Math.toRadians(r))), paint);
}
paint.setColor(0x44d4b801);
canvas.drawCircle(centre, centre, radius - DPUnitUtil.dip2px(mContext, 2f), paint); //畫出圓環
paint.setColor(mColorBackground);
canvas.drawCircle(centre, centre, radius - DPUnitUtil.dip2px(mContext, 2.5f), paint); //畫出圓環
//到此,背景繪製完畢
String per = (int) now + "";
//寫百分比
if ("".equals(strprogress)) {
paint.setColor(mColor);
paint.setStrokeWidth(DPUnitUtil.dip2px(mContext, 2));
canvas.drawLine(centre * 0.77f, centre, centre * 0.95f, centre, paint);
canvas.drawLine(centre * 1.05f, centre, centre * 1.23f, centre, paint);
} else {
paint.setColor(mColor);
paint.setTextSize(mLayoutSize / 4f);//控制文字大小
Paint paint2 = new Paint();
paint2.setAntiAlias(true);
paint2.setTextSize(mLayoutSize / 12);//控制文字大小
paint2.setColor(mColor);
canvas.drawText(per,
centre - 0.5f * (paint.measureText(per)),
centre - 0.5f * (paint.ascent() + paint.descent()),
paint);
canvas.drawText("分",
centre + 0.5f * (paint.measureText((int) now + "") + paint2.measureText("分")),
centre - 0.05f * (paint.ascent() + paint.descent()),
paint2);
}
//外部小球
centre = getWidth() / 2;
canvas.drawCircle(centre + (float) ((centre * 0.95f) * Math.sin(Math.toRadians(endR))),
centre - (float) ((centre * 0.95f) * Math.cos(Math.toRadians(endR))), centre * 0.04f, paint);
Path p = new Path();
p.moveTo(centre + (float) ((centre * 0.86f) * Math.sin(Math.toRadians(endR))),
centre - (float) ((centre * 0.86f) * Math.cos(Math.toRadians(endR))));
p.lineTo(centre + (float) ((centre * 0.94f) * Math.sin(Math.toRadians(endR + 2.5))),
centre - (float) ((centre * 0.94f) * Math.cos(Math.toRadians(endR + 2.5))));
p.lineTo(centre + (float) ((centre * 0.94f) * Math.sin(Math.toRadians(endR - 2.5))),
centre - (float) ((centre * 0.94f) * Math.cos(Math.toRadians(endR - 2.5))));
p.close();
canvas.drawPath(p, paint);
if (now < progress - 1) {
now = now + 1;
postInvalidate();
} else if (now < progress) {
now = (int) progress;
postInvalidate();
}
}
/**
* 外部回調
*
* @param strprogress 顯示調進度文字,若是是"",或者null了,則顯示兩條橫線
* @param progress 進度條調進度
* @param isAnim 進度條是否須要動畫
*/
public void setProgress(String strprogress, float progress, boolean isAnim) {
if (strprogress == null) {
this.strprogress = "";
} else {
this.strprogress = strprogress;
}
this.now = 0;
this.progress = progress;
if (!isAnim) {
now = progress;
}
postInvalidate();
}
}
複製代碼