最近很长一段时间都在撸自定义View,说实话,在成为大牛的路上,这一块是必走之路,而作为菜鸡的我必须把它啃下来,俗话说不会自定义View的大牛不是好的程序猿,所以呢,强撸吧!!!GitHub传送门:https://github.com/SuperKotlin/CirclrProgress
先来看一张漂亮妹子的图片,额呸呸呸,不对,是来看一张效果图图片(手动滑稽)。
circlrProgress.gif
看到这样的一个效果,该如何去实现呢?下面我就一步一步的把她撸出来!
思路
1.自定义属性:文字的颜色和字体大小,圆弧的颜色和宽度,一开始加载进度的位置,等等; 2.画出需要的效果:画圆弧,画字体,使用画笔paint在canvas上绘制; 3.设置进度,重新绘制; 4.接口回调。
好的,接下来我们就按照这个思路一步一步的撸:
1.自定义我们需要的属性:
在values文件夹下新建文件attrs.xml
<declare-styleable name="CircleProgressView"> <!--画笔宽度--> <attr name="progress_paint_width" format="dimension" /> <!--画笔颜色--> <attr name="progress_paint_color" format="color" /> <!--字体颜色--> <attr name="progress_text_color" format="color" /> <!--字体尺寸--> <attr name="progress_text_size" format="dimension" /> <!--加载进度的开始位置--> <attr name="location" format="enum"> <enum name="left" value="1" /> <enum name="top" value="2" /> <enum name="right" value="3" /> <enum name="bottom" value="4" /> </attr> </declare-styleable>
然后在自定义View中获取并设置这些属性:
首先,来声明我们的属性类型:
private int mCurrent;//当前进度 private Paint mPaintOut; private Paint mPaintCurrent; private Paint mPaintText; private float mPaintWidth;//画笔宽度 private int mPaintColor = Color.RED;//画笔颜色 private int mTextColor = Color.BLACK;//字体颜色 private float mTextSize;//字体大小 private int location;//从哪个位置开始 private float startAngle;//开始角度
获取自定义属性值:
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressView); location = array.getInt(R.styleable.CircleProgressView_location, 1); mPaintWidth = array.getDimension(R.styleable.CircleProgressView_progress_paint_width, dip2px(context, 4));//默认4dp mPaintColor = array.getColor(R.styleable.CircleProgressView_progress_paint_color, mPaintColor); mTextSize = array.getDimension(R.styleable.CircleProgressView_progress_text_size, dip2px(context, 18));//默认18sp mTextColor = array.getColor(R.styleable.CircleProgressView_progress_text_color, mTextColor); array.recycle();
初始化和赋值:
//画笔->背景圆弧 mPaintOut = new Paint(); mPaintOut.setAntiAlias(true); mPaintOut.setStrokeWidth(mPaintWidth); mPaintOut.setStyle(Paint.Style.STROKE); mPaintOut.setColor(Color.GRAY); mPaintOut.setStrokeCap(Paint.Cap.ROUND); //画笔->进度圆弧 mPaintCurrent = new Paint(); mPaintCurrent.setAntiAlias(true); mPaintCurrent.setStrokeWidth(mPaintWidth); mPaintCurrent.setStyle(Paint.Style.STROKE); mPaintCurrent.setColor(mPaintColor); mPaintCurrent.setStrokeCap(Paint.Cap.ROUND); //画笔->绘制字体 mPaintText = new Paint(); mPaintText.setAntiAlias(true); mPaintText.setStyle(Paint.Style.FILL); mPaintText.setColor(mTextColor); mPaintText.setTextSize(mTextSize);
2.画出需要的效果:画圆弧,画字体,使用画笔paint在canvas上绘制:
注意:绘制操作是在onDraw(Canvas canvas)方法中。
第一步:绘制背景灰色圆弧:
我们使用cancas的drawArc()方法,来了解一下这个方法是什么意思:
public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint){}
oval // 绘制范围 startAngle // 开始角度 sweepAngle // 扫过角度 useCenter // 是否使用中心
至于这些都是什么意思,我讲一堆未必能懂,怎么办?
举个栗子.jpg
假设是这样:canvas.drawArc(rectF, 45, 90, false, mPaintOut);
代表:从45°开始,扫过90°范围,方向是顺时针方向绘制,至于那个false是什么意思,暂时先不管,传true一般画扇形图才能用到。来看看草图:
one.jpg
好的,一目了然,如果我们要画一个圆圈怎么办呢,很简单,扫过的范围是360°就OK了。起点在哪个位置就无所谓了。看到这里有些人就要问了,画圆为什么不使用canvas.drawCircle()方法,这是因为后面要画的圆弧使用的也是drawArc()方法,所以为了易懂我们都用这个吧(手动大笑)。
//绘制背景圆弧,因为画笔有一定的宽度,所有画圆弧的范围要比View本身的大小稍微小一些,不然画笔画出来的东西会显示不完整 RectF rectF = new RectF(mPaintWidth / 2, mPaintWidth / 2, getWidth() - mPaintWidth / 2, getHeight() - mPaintWidth / 2); canvas.drawArc(rectF, 0, 360, false, mPaintOut);
第二步:绘制当前进度的圆弧
这里有人就发现了,我们还继续按照上面的方式只要传入不同的sweepAngle扫过角度的值不就好了吗?yes,没错。我们只要计算出当前百分比所对应的角度值是多少度就OK了。很简单的一个公式:画个草图吧!
two.jpg
那么代码就很容易写了。
//绘制当前进度 float sweepAngle = 360 * mCurrent / 100; canvas.drawArc(rectF, startAngle, sweepAngle, false, mPaintCurrent);
第三步:绘制文字
先来看看canvas的方法:
// 参数分别为 (文本 基线x 基线y 画笔) canvas.drawText(String text, float x, float y,Paint paint);
基线是个什么鬼,画个草图吧!
three.jpg
说白了就是文字左下角的那个点的坐标。
而我们要把文字画在View的中心点位置,所以开始撸吧,
来,先求x点坐标=View宽度的一半减去文字宽度的一半,思考一下是不是?
y点的坐标=View高度的一半+文字高度的一半。
OK,上代码:
//绘制进度数字 String text = mCurrent + "%"; //获取文字宽度 float textWidth = mPaintText.measureText(text, 0, text.length()); float dx = getWidth() / 2 - textWidth / 2; Paint.FontMetricsInt fontMetricsInt = mPaintText.getFontMetricsInt(); float dy = (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom; float baseLine = getHeight() / 2 + dy; canvas.drawText(text, dx, baseLine, mPaintText);
为什么这么写,说白了都是套路。。。。。。
3.设置进度,重新绘制;
为了让当前进度mCurrent从0~100的增加,我们需要暴露一个方法可以实时的设置mCurrent值然后不断的进行绘制界面。
/** * 设置当前进度并重新绘制界面 * * @param mCurrent */ public void setmCurrent(int mCurrent) { this.mCurrent = mCurrent; invalidate(); }
相信很所人都知道只要调用了 invalidate()方法,正常情况下系统就会调用onDraw方法,然后就可以不断的绘制界面了。
这里写个属性动画来达到进度条加载的效果:
//进度条从0到100 ValueAnimator animator = ValueAnimator.ofFloat(0, 100); animator.setDuration(4000); animator.setInterpolator(new LinearInterpolator()); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float current = (float) animation.getAnimatedValue(); mCircleProgressView.setmCurrent((int) current); } }); animator.start();
4.接口回调。
这一步就很简单了,只要监听进度条达到100就是完成了加载,所以我们先来写一个接口。
//声明接口 public interface OnLoadingCompleteListener { void complete(); } //暴露回调方法 public void setOnLoadingCompleteListener(OnLoadingCompleteListener loadingCompleteListener) { this.mLoadingCompleteListener = loadingCompleteListener; } //监听 if (mLoadingCompleteListener != null && mCurrent == 100) { mLoadingCompleteListener.complete(); }
在对应的Activity中回调接口就可以了。
OK,到这里就算全部结束了,上一张全家福吧:
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" tools:context="com.zhuyong.circleprogress.MainActivity"> <com.zhuyong.circleprogress.CircleProgressView android:id="@+id/circle_view" android:layout_width="150dp" android:layout_height="150dp" app:location="left" app:progress_paint_color="#FF4081" app:progress_paint_width="4dp" app:progress_text_color="#3F51B5" app:progress_text_size="16sp" /> </LinearLayout>
MainActivity.java:
public class MainActivity extends AppCompatActivity { private CircleProgressView mCircleProgressView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mCircleProgressView = (CircleProgressView) findViewById(R.id.circle_view); //进度条从0到100 ValueAnimator animator = ValueAnimator.ofFloat(0, 100); animator.setDuration(4000); animator.setInterpolator(new LinearInterpolator()); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float current = (float) animation.getAnimatedValue(); mCircleProgressView.setmCurrent((int) current); } }); animator.start(); mCircleProgressView.setOnLoadingCompleteListener(new CircleProgressView.OnLoadingCompleteListener() { @Override public void complete() { Toast.makeText(MainActivity.this, "加载完成", Toast.LENGTH_SHORT).show(); } }); } }
CircleProgressView.java:
import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.View; /** * Created by zhuyong on 2017/6/22. */ public class CircleProgressView extends View { private int mCurrent;//当前进度 private Paint mPaintOut; private Paint mPaintCurrent; private Paint mPaintText; private float mPaintWidth;//画笔宽度 private int mPaintColor = Color.RED;//画笔颜色 private int mTextColor = Color.BLACK;//字体颜色 private float mTextSize;//字体大小 private int location;//从哪个位置开始 private float startAngle;//开始角度 private OnLoadingCompleteListener mLoadingCompleteListener; public CircleProgressView(Context context) { this(context, null); } public CircleProgressView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public CircleProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressView); location = array.getInt(R.styleable.CircleProgressView_location, 1); mPaintWidth = array.getDimension(R.styleable.CircleProgressView_progress_paint_width, dip2px(context, 4));//默认4dp mPaintColor = array.getColor(R.styleable.CircleProgressView_progress_paint_color, mPaintColor); mTextSize = array.getDimension(R.styleable.CircleProgressView_progress_text_size, dip2px(context, 18));//默认18sp mTextColor = array.getColor(R.styleable.CircleProgressView_progress_text_color, mTextColor); array.recycle(); //画笔->背景圆弧 mPaintOut = new Paint(); mPaintOut.setAntiAlias(true); mPaintOut.setStrokeWidth(mPaintWidth); mPaintOut.setStyle(Paint.Style.STROKE); mPaintOut.setColor(Color.GRAY); mPaintOut.setStrokeCap(Paint.Cap.ROUND); //画笔->进度圆弧 mPaintCurrent = new Paint(); mPaintCurrent.setAntiAlias(true); mPaintCurrent.setStrokeWidth(mPaintWidth); mPaintCurrent.setStyle(Paint.Style.STROKE); mPaintCurrent.setColor(mPaintColor); mPaintCurrent.setStrokeCap(Paint.Cap.ROUND); //画笔->绘制字体 mPaintText = new Paint(); mPaintText.setAntiAlias(true); mPaintText.setStyle(Paint.Style.FILL); mPaintText.setColor(mTextColor); mPaintText.setTextSize(mTextSize); if (location == 1) {//默认从左侧开始 startAngle = -180; } else if (location == 2) { startAngle = -90; } else if (location == 3) { startAngle = 0; } else if (location == 4) { startAngle = 90; } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); int size = width > height ? height : width; setMeasuredDimension(size, size); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制背景圆弧,因为画笔有一定的宽度,所有画圆弧的范围要比View本身的大小稍微小一些,不然画笔画出来的东西会显示不完整 RectF rectF = new RectF(mPaintWidth / 2, mPaintWidth / 2, getWidth() - mPaintWidth / 2, getHeight() - mPaintWidth / 2); canvas.drawArc(rectF, 0, 360, false, mPaintOut); //绘制当前进度 float sweepAngle = 360 * mCurrent / 100; canvas.drawArc(rectF, startAngle, sweepAngle, false, mPaintCurrent); //绘制进度数字 String text = mCurrent + "%"; //获取文字宽度 float textWidth = mPaintText.measureText(text, 0, text.length()); float dx = getWidth() / 2 - textWidth / 2; Paint.FontMetricsInt fontMetricsInt = mPaintText.getFontMetricsInt(); float dy = (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom; float baseLine = getHeight() / 2 + dy; canvas.drawText(text, dx, baseLine, mPaintText); if (mLoadingCompleteListener != null && mCurrent == 100) { mLoadingCompleteListener.complete(); } } /** * 获取当前进度值 * * @return */ public int getmCurrent() { return mCurrent; } /** * 设置当前进度并重新绘制界面 * * @param mCurrent */ public void setmCurrent(int mCurrent) { this.mCurrent = mCurrent; invalidate(); } public void setOnLoadingCompleteListener(OnLoadingCompleteListener loadingCompleteListener) { this.mLoadingCompleteListener = loadingCompleteListener; } public interface OnLoadingCompleteListener { void complete(); } /** * 根据手机的分辨率从 dp 的单位 转成为 px(像素) */ public static int dip2px(Context context, float dpValue) { final float scale = context.getResources().getDisplayMetrics().density; return (int) (dpValue * scale + 0.5f); } }