Android 自定义view之圆盘进度条
程序员文章站
2022-03-31 10:29:21
很久没有用到自定义View了,手有点生疏了,这不同事刚扔给一个活,按照UI的要求,画一个进度条,带动画效果的。需求是这样的:
嗯,实现后效果如下:
嗯,算是基本满足...
很久没有用到自定义View了,手有点生疏了,这不同事刚扔给一个活,按照UI的要求,画一个进度条,带动画效果的。需求是这样的:
嗯,实现后效果如下:
嗯,算是基本满足需求吧。
本文包含的知识点
1、自定义view的绘制
2、属性动画
3、图像的合成模式 PorterDuff.Mode
嗯,废话不多说,show me the code
1)WordView.java
import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.graphics.Xfermode; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.Log; import android.view.View; /** * Author: gongwq on 2017/12/20 002011. * Email: gwq_0111@163.com * Descriptions: */ public class WordView extends View { private Paint linePaint, circlePaint, circlePaintBg; private Paint textPaint1, textPaint2, textPaint3, textPaint4; private int screenWidth, screenHeight; private int mCenter, mRadius; private RectF mRectF, mRectFBg1, mRectFBg2; private int defaultValue; private float rate = 0f; private String hasStudyText = ""; private String notStudyText = ""; private int startAng = 0; private int defaultStartAng = 120; private long animDuration = 2000L; private int progressColor, wordTitleColor, wordViewBackground; private Xfermode xfermode; public WordView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); TypedArray ta = context.obtainStyledAttributes( attrs, R.styleable.WordView); progressColor = ta.getColor(R.styleable.WordView_progressColor, 0xFFFFFFFF); wordViewBackground = ta.getColor(R.styleable.WordView_wordViewBackground, 0xFFFF0000); wordTitleColor = ta.getColor(R.styleable.WordView_wordTitleColor, 0xFFFF00FF); ta.recycle(); initPaint(context); } private void initPaint(Context context) { screenWidth = MeasureUtil.getScreenWidth(context); screenHeight = MeasureUtil.getScreenHeight(context); linePaint = new Paint(); linePaint.setColor(Color.WHITE); linePaint.setStyle(Paint.Style.FILL); linePaint.setAntiAlias(true); linePaint.setStrokeWidth(6.0f); circlePaint = new Paint(); circlePaint.setColor(progressColor); circlePaint.setAntiAlias(true); circlePaint.setStyle(Paint.Style.STROKE); circlePaint.setStrokeCap(Paint.Cap.ROUND); circlePaint.setStrokeWidth(6.0f); circlePaintBg = new Paint(); circlePaintBg.setColor(wordViewBackground); circlePaintBg.setAntiAlias(true); circlePaintBg.setStyle(Paint.Style.FILL); textPaint1 = new Paint();//已学习生词 textPaint1.setColor(wordTitleColor); textPaint1.setTextAlign(Paint.Align.CENTER); textPaint1.setAntiAlias(true); textPaint1.setTextSize(25); textPaint2 = new Paint();//xx个 textPaint2.setColor(Color.WHITE); textPaint2.setTextAlign(Paint.Align.CENTER); textPaint2.setAntiAlias(true); textPaint2.setTextSize(70); textPaint4 = new Paint();//个 textPaint4.setColor(Color.WHITE); textPaint4.setTextAlign(Paint.Align.CENTER); textPaint4.setAntiAlias(true); textPaint4.setTextSize(25); textPaint3 = new Paint();//未学习生词x个 textPaint3.setColor(wordTitleColor); textPaint3.setTextAlign(Paint.Align.CENTER); textPaint3.setAntiAlias(true); textPaint3.setTextSize(25); mCenter = screenWidth / 2; mRadius = screenWidth / 4; mRectF = new RectF(10, 10, 2 * mRadius - 10, 2 * mRadius - 10);//外切圆弧 mRectFBg1 = new RectF(20, 20, 2 * mRadius - 20, 2 * mRadius - 20);//中间的背景 mRectFBg2 = new RectF(60, 2 * mRadius - 60, 2 * mRadius - 60, 2 * mRadius + 60);//通过这个去调下面“缺”的弧度 xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);//DST_OUT在不相交的地方绘制目标图像,相交处根据源图像alpha进行过滤,完全不透明处则完全过滤,完全透明则不过滤 } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawArc(mRectF, startAng, (360 - (startAng - 90) * 2) * rate, false, circlePaint); //将绘制操作保存到新的图层,将图像合成的处理放到离屏缓存中进行 circlePaintBg.setColor(wordViewBackground); int saveCount = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG); canvas.drawArc(mRectFBg1, 0, 360, true, circlePaintBg);//绘制目标图 circlePaintBg.setXfermode(xfermode);//设置混合模式 circlePaintBg.setColor(Color.parseColor("#FF00FF00"));//这里的颜色只需前面的透明值为FF即完全不透明即可 canvas.drawArc(mRectFBg2, 0, 360, true, circlePaintBg);//绘制源图 circlePaintBg.setXfermode(null);//清除混合模式 canvas.restoreToCount(saveCount); canvas.drawText("已学习生词", mRadius, mRectF.top + 100, textPaint1); float width = textPaint2.measureText(hasStudyText); float width2 = textPaint4.measureText("个"); // float total = mRectFBg1.right - mRectFBg1.left; // Log.e("文字宽度---->", width + " " + width2 + " " + total); // canvas.drawText(hasStudyText, mRadius + 30, mRadius, textPaint2); float center1 = mRadius - (width + width2) / 2 + width / 2; float center2 = mRadius - (width + width2) / 2 + width + width2 / 2; canvas.drawText(hasStudyText, center1, mRadius + 20, textPaint2); canvas.drawText(notStudyText, mRadius, mRectF.bottom - 100, textPaint3); if (startAng != 0) { canvas.drawText("个", center2, mRadius + 20, textPaint4); canvas.rotate(180 + (startAng - 90) + (360 - (startAng - 90) * 2) * rate, mRadius, mRadius); if (Integer.parseInt(hasStudyText) > 0) { canvas.drawLine(mRadius, mRectF.top, mRadius, mRectF.top + 20, linePaint); } } } /** * 设置学习单词数 * * @param hasStudyNum 已学习单词数 * @param notStudyNum 未学习单词数 */ public void setWordsNum(final int hasStudyNum, final int notStudyNum) { rate = (float) hasStudyNum / (float) (hasStudyNum + notStudyNum); final float rate2 = rate; startAng = getStartAng();//startAng必须为大于等于90,小于180 ValueAnimator anim = ValueAnimator.ofFloat(1, 100); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { rate = (float) animation.getAnimatedValue() * rate2 / 100f; hasStudyText = (int) ((float) animation.getAnimatedValue() * hasStudyNum / 100) + ""; notStudyText = "未学习生词" + (int) ((float) animation.getAnimatedValue() * notStudyNum / 100) + "个"; postInvalidate(); } }); // hasStudyText = hasStudyNum + ""; // notStudyText = "未学习生词" + notStudyNum+"个"; anim.setDuration(getAnimationDuration()); anim.start(); } /** * 设置圆弧的起始角度值 注 1.值必须是[90,180] 2.必须在setWordNum()方法之前调用 * @param ang */ public void setStartAng(int ang) { this.startAng = ang; } public int getStartAng() { if (startAng == 0) { return defaultStartAng; } return startAng; } /** * 设置动画时间 ,注意 需要在setWordsNum前调用才会生效 * * @param time */ public void setAnimationDuration(long time) { this.animDuration = time; } public long getAnimationDuration(){ return animDuration; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); } private int measureWidth(int widthMeasureSpec) { int mode = MeasureSpec.getMode(widthMeasureSpec); int size = MeasureSpec.getSize(widthMeasureSpec); // 默认宽高; defaultValue = screenWidth; switch (mode) { case MeasureSpec.AT_MOST: // 最大值模式 当控件的layout_Width或layout_height属性指定为wrap_content时 // Log.e("cmos---->", "size " + size + " screenWidth " + screenWidth); // size = Math.min(defaultValue, size); size = screenWidth / 2; defaultValue = size; break; case MeasureSpec.EXACTLY: // 精确值模式 // 当控件的android:layout_width=”100dp”或android:layout_height=”match_parent”时 break; default: size = defaultValue; break; } return size; } private int measureHeight(int heightMeasureSpec) { int mode = MeasureSpec.getMode(heightMeasureSpec); int size = MeasureSpec.getSize(heightMeasureSpec); switch (mode) { case MeasureSpec.AT_MOST: // 最大值模式 当控件的layout_Width或layout_height属性指定为wrap_content时 Log.e("cmos---->", "size " + size + " screenHeight " + screenHeight); // size = Math.min(screenHeight / 2, size); size = defaultValue; break; case MeasureSpec.EXACTLY: // 精确值模式 // 当控件的android:layout_width=”100dp”或android:layout_height=”match_parent”时 break; default: size = defaultValue; break; } return size; } }
下面是他的一些配置
attrs.xml
布局文件
activity_main.xml
使用
MainActivity.java
...... @Override public void onClick(View v) { switch (v.getId()) { case R.id.bt: if (!TextUtils.isEmpty(et1.getText().toString()) && !TextUtils.isEmpty(et2.getText().toString())) { int num1 = Integer.parseInt(et1.getText().toString()); int num2 = Integer.parseInt(et2.getText().toString()); wordView.setAnimationDuration(4000); wordView.setStartAng(130); wordView.setWordsNum(num1, num2); } else { Toast.makeText(this, "数值不能为空", Toast.LENGTH_SHORT).show(); } break; } } ......
代码里注释已经相对较清晰了,就不做解释了,有不懂的可以留言。
整个view的重点就在onDraw()方法里,怎么去放置文字,中间的“xxx个”怎么随着数字的长度变化而始终居中,这主要与initPaint()画笔方法有关,其中textPaint.setTextAlign(Paint.Align.CENTER);是重点,它表示画的文字,你后面给定他一个绘制的中心点,然后它的文字会自动居中。第二个要注意的地方是,中间的背景,我是画的,开始准备用UI给的背景的,但是发现不好适配,所以就自己画了,这里主要用到的是图像合成模式PorterDuff.Mode,图像的合成模式的枚举类一共有16种,通过这16种模式,我们可以自己根据给定的2个图片,合成我们想要的结果。这也给我们一个启示:当需求中的图片可以看成是多个图片组合成的结果的画,不妨可以试试 图像的合成模式PorterDuff.Mode。