照虎画猫仿支付宝信用仪表盘
这几天闲来无事,决定好好复习一下自定义控件
然后再抄一个自定义控件
仿支付宝信用仪表盘
我知道没有效果图你们是不会往下看的.
开始照虎画猫:
新建一个类继承view,重写构造:
public class RoundView extends View {
public RoundView(Context context) {
this(context, null);
}
public RoundView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public RoundView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAttrs(context, attrs);
init();
}
自定义属性:
values下新建attrs.xml文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RoundView">
<!--最大数值-->
<attr name="maxNum" format="integer"/>
<!--圆盘起始角度-->
<attr name="startAngle" format="integer"/>
<!--圆盘扫过的角度-->
<attr name="sweepAngle" format="integer"/>
</declare-styleable>
</resources>
代码中初始化自定义属性:
private void initAttrs(Context context, @Nullable AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RoundView);
mMaxNum = typedArray.getInteger(R.styleable.RoundView_maxNum, 500);
mStartAngle = typedArray.getInteger(R.styleable.RoundView_startAngle, 160);
mSweepAngle = typedArray.getInteger(R.styleable.RoundView_sweepAngle, 220);
typedArray.recycle();
}
布局中使用:
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.example.walle9.roundindicatorview.RoundView
android:id="@+id/RoundView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#e55e10"
app:maxNum="500"
app:startAngle="160"
app:sweepAngle="220"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/et"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#fff"/>
<Button
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:onClick="click"
android:text="查看"/>
</LinearLayout>
</LinearLayout>
这里我们自定义控件设置了宽高同时又设置了权重,这样下面的控件刚好包裹住,剩下的都分配给了自定义控件
初始化画笔:
private void init() {
//内外圆弧的宽度
mSweepInWidth = UIUtils.dip2Px(15);
mSweepOutWidth = UIUtils.dip2Px(5);
//抗锯齿
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setDither(true); //抖动
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(0xffffffff);
mPaint_2 = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint_3 = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint_4 = new Paint(Paint.ANTI_ALIAS_FLAG);
}
设置Flag的两种方法:
第一种:
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
1
第二种:
Paint paint = new Paint();
paint.setFlags(Paint.ANTI_ALIAS_FLAG);
1
2
几种Flag意义
Paint.ANTI_ALIAS_FLAG :抗锯齿标志
Paint.FILTER_BITMAP_FLAG : 使位图过滤的位掩码标志
Paint.DITHER_FLAG : 使位图进行有利的抖动的位掩码标志
Paint.UNDERLINE_TEXT_FLAG : 下划线
Paint.STRIKE_THRU_TEXT_FLAG : 中划线
Paint.FAKE_BOLD_TEXT_FLAG : 加粗
Paint.LINEAR_TEXT_FLAG : 使文本平滑线性扩展的油漆标志
Paint.SUBPIXEL_TEXT_FLAG : 使文本的亚像素定位的绘图标志
Paint.EMBEDDED_BITMAP_TEXT_FLAG : 绘制文本时允许使用位图字体的绘图标志
也可以使用setAntiAlias(boolean aa)方法设置抗锯齿.
各种set方法参见:
paint的各种set
setDither()抖动
抖动 抗锯齿
重写onMeasure方法
//对于不是确定值的直接给定320*480的大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
mWidth = widthSize;
} else {
mWidth = 320;
}
if (heightMode == MeasureSpec.EXACTLY) {
mHeight = heightSize;
} else {
mHeight = 480;
}
setMeasuredDimension(mWidth, mHeight);//设置最终宽高
}
重写onDraw方法:
把画布移动到中间位置
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mRadius = getMeasuredWidth() / 3;
canvas.save();
canvas.translate(mWidth / 2, mHeight / 2);
drawRound(canvas);//画圆弧
drawScale(canvas);//画刻度
drawIndicator(canvas);//画进度
drawCenterText(canvas);//画信用值
canvas.restore();
}
下面我们一个一个看绘制方法:
drawRound画圆弧
private void drawRound(Canvas canvas) {
//画内圆
mPaint.setAlpha(0x44);//设置透明度,范围是00~ff
mPaint.setStrokeWidth(mSweepInWidth);
RectF rectF = new RectF(-mRadius, -mRadius, mRadius, mRadius);
canvas.drawArc(rectF, mStartAngle, mSweepAngle, false, mPaint);
//画外圆
mPx = UIUtils.dip2Px(15);
mPaint.setStrokeWidth(mSweepOutWidth);
RectF rectFO = new RectF(-mRadius - mPx, -mRadius - mPx, mRadius + mPx, mRadius + mPx);
canvas.drawArc(rectFO, mStartAngle, mSweepAngle, false, mPaint);
}
效果如下:
drawScale画刻度
我们观察一下原图,粗的刻度线一共有6条,数字的刻度是再粗刻度线下面的,每两个粗刻度线之间有5条细刻度线,并且中间那条细刻度线下方有对应文字。我们把扫过的角度除以30,就是每个刻度的间隔了,然后通过判断就可以画对应刻度和文字了。
private String[] text = {"较差", "中等", "良好", "优秀", "极好"};
private void drawScale(Canvas canvas) {
float angle = mSweepAngle / 30.0f;//刻度间隔
canvas.save();
canvas.rotate(-90 - (180 - mStartAngle));//-270+mStartAngle
//逆时针旋转90度,再逆时针旋转没旋转的度数,注意,此时的旋转和已经绘制好的图形无关了.
//(可以在不旋转的时候在0,0绘制一个文字,然后旋转90,和-90各看一下文字方向)
//其实你可以这样理解(-270+mStartAngle),把它看成(-90-(180 - mStartAngle)),
// 先旋转-90度,文字方向是不是头在左边脚在右边了,再把剩下的没旋转过去的继续旋转过去即可.
//还有不要想着之前的那个画好的外圈和内圈会怎么怎么转,我们之前save了一把,所以跟他那一层没有半毛钱的关系
for (int i = 0; i <= 30; i++) {
if (i % 6 == 0) {//画粗刻度和刻度值
mPaint.setStrokeWidth(UIUtils.dip2Px(2));
mPaint.setAlpha(0x70);
canvas.drawLine(0, -mRadius - mSweepInWidth / 2, 0, -mRadius + mSweepInWidth / 2
+ UIUtils.dip2Px(2), mPaint);
drawText(canvas, i * mMaxNum / 30 + "", mPaint);
} else {//画小刻度
mPaint.setStrokeWidth(UIUtils.dip2Px(1));
mPaint.setAlpha(0x50);
canvas.drawLine(0, -mRadius - mSweepInWidth / 2, 0, -mRadius + mSweepInWidth / 2,
mPaint);
if ((i - 3) % 6 == 0) { //画刻度区间文字
mPaint.setStrokeWidth(UIUtils.dip2Px(2));
drawText(canvas, text[(i - 3) / 6], mPaint);
}
}
canvas.rotate(angle);
}
canvas.restore();
}
其中:
private void drawText(Canvas canvas, String text, Paint paint) {
paint.setStyle(Paint.Style.FILL);
paint.setTextSize(UIUtils.dip2Px(15));
paint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(text, 0, -mRadius + UIUtils.dip2Px(25), mPaint);
paint.setStyle(Paint.Style.STROKE);
}
对于绘制文字,我们使用setTextAlign设置绘制坐标的起点为中间,这样我们就不用去测量文字的大小了.
看下效果:
drawIndicator绘制进度
对于进度颜色的渐变,我们使用paint的着色器shader渲染,
它有5个子类
BitmapShader位图
LinearGradient线性渐变
RadialGradient径向渐变
SweepGradient梯度渐变
ComposeShader混合渐变
详情参考:
Android中Canvas绘图之Shader使用图文详解
详解Paint的setShader(Shader shader)
Android 图像处理(一) : Shader
我们这里使用SweepGradient,梯度渐变,也称扫描渐变,
SweepGradient可以用来创建360度颜色旋转渐变效果,具体来说颜色是围绕中心点360度顺时针旋转的,起点就是3点钟位置。
SweepGradient有两个构造函数:
SweepGradient(float cx, float cy, int color0, int color1)
SweepGradient(float cx, float cy, int[] colors, float[] positions)
我们这里使用第二个构造函数
在SweepGradient的第二个构造函数中,我们可以传入一个colors颜色数组,这样Android就会根据传入的颜色数组一起进行颜色插值。还可以指定positions数组,该数组中每一个position对应colors数组中每个颜色在360度中的相对位置,position取值范围为[0,1],0和1都表示3点钟位置,0.25表示6点钟位置,0.5表示9点钟位置,0.75表示12点钟位置,诸如此类。如果positions数组为null,那么Android会自动为colors设置等间距的位置。
当然,起点颜色的位置不一定是0,终点颜色的位置也不一定是1,我们将positions数组改为如下所示:
float[] positions = {0.25f, 0.5f, 0.75f};
那么0.25f之前的颜色,和0.75之后的颜色都会被开始和结束的颜色填充,并不会什么都没有,这个需要注意.
对于小圆点有光源一样的边缘模糊效果,我们使用paint的setMaskFilter,其中有一个子类BlurMaskFilter模糊遮罩滤镜可以实现边缘模糊效果,这是一个过时方法,不支持硬件加速,
我们可以在View中通过
setLayerType(LAYER_TYPE_SOFTWARE, null);
只针对某个View关闭硬件加速
或者在清单文件中的activity中使用
android:hardwareAccelerated="false"
关闭activity的硬件加速
具体请参考:
Android Paint之MaskFilter详解
详解Paint的setMaskFilter(MaskFilter maskfilter)
自定义控件其实很简单1/4
看下代码:
private void drawIndicator(Canvas canvas) {
int sweep;//当前扫过的弧度
if (currentNum <= mMaxNum) {
sweep = (int) (currentNum * mSweepAngle / (float) mMaxNum);
} else {
sweep = mSweepAngle;
}
canvas.save();
if (mStartAngle + sweep > 360) {//当我们角度>360度的时候,我们不能继续渐变,因为渐变就是从0度开始的
//开始旋转
canvas.rotate(mStartAngle + sweep - 360);//当前的角度超过360度几度我们就旋转几度
//那开始角度又透明了,处理不处理随便了.
}
mPaint_2.setStyle(Paint.Style.STROKE);
mPaint_2.setStrokeWidth(mSweepOutWidth);
int[] colors = {0x00ffffff, Color.GREEN, 0x00ffffff};
float[] positions = {mStartAngle / 360.f, (mStartAngle + sweep) / 360.f, (mStartAngle +
sweep) / 360.f};
Shader shader = new SweepGradient(0, 0, colors, positions);
mPaint_2.setShader(shader);
RectF rectFO = new RectF(-mRadius - mPx, -mRadius - mPx, mRadius + mPx, mRadius + mPx);
canvas.drawArc(rectFO, mStartAngle, mSweepAngle, false, mPaint_2);
mPaint_2.setStyle(Paint.Style.FILL);
//canvas.drawCircle(0,0,mRadius+mPx,mPaint_2);//我们这里下半部分是透明的,实际上也可以直接画圆环
canvas.restore();
canvas.save();
//画一个亮闪闪的小球把!
mPaint_3.setStyle(Paint.Style.FILL);
mPaint_3.setColor(0xffffffff);
//当前的弧度
int radians = mStartAngle + sweep;
float y = (float) ((mRadius + mPx) * Math.sin(Math.toRadians(radians)));
float x = (float) ((mRadius + mPx) * Math.cos(Math.toRadians(radians)));
//设置模糊遮罩滤镜,记得要关闭硬件加速
mPaint_3.setMaskFilter(new BlurMaskFilter(UIUtils.dip2Px(5), BlurMaskFilter.Blur.SOLID));
//关闭此VIEW的硬件加速,也可以在清单文件中使用android:hardwareAccelerated="false"关闭activity的硬件加速
setLayerType(LAYER_TYPE_SOFTWARE, null);
canvas.drawCircle(x, y, UIUtils.dip2Px(4), mPaint_3);
canvas.restore();
}
看下效果:
最后我们来画信用值drawCenterText
对于text的测量:
使用measureText:只能测量字符串的宽度,不能测量高度
使用getTextBounds可以测量出字符串的左上右下
private void drawCenterText(Canvas canvas) {
canvas.save();
mPaint_4.setStyle(Paint.Style.FILL);
mPaint_4.setTextSize(mRadius / 2);
mPaint_4.setColor(0x99ffffff);
canvas.drawText(currentNum + "", -mPaint_4.measureText(currentNum + "") / 2, 0, mPaint_4);
//x值传入测量宽度除以2(这种测量只能测量宽度)或者像绘制度数的时候传入0,然后setTextAlign(Paint.Align.CENTER)
mPaint_4.setTextSize(mRadius / 4);
String content = "信用";
if (currentNum < mMaxNum * 1 / 5) {
content += text[0];
} else if (currentNum >= mMaxNum * 1 / 5 && currentNum < mMaxNum * 2 / 5) {
content += text[1];
} else if (currentNum >= mMaxNum * 2 / 5 && currentNum < mMaxNum * 3 / 5) {
content += text[2];
} else if (currentNum >= mMaxNum * 3 / 5 && currentNum < mMaxNum * 4 / 5) {
content += text[3];
} else if (currentNum >= mMaxNum * 4 / 5) {
content += text[4];
}
//使用这种测量可以测量出左上右下
Rect r = new Rect();
mPaint_4.getTextBounds(content, 0, content.length(), r);
canvas.drawText(content, -r.width() / 2, r.height() + 20, mPaint_4);
canvas.restore();
}
看下效果:
发现好多大神都把自定义控件和动画放在一起写博客,现在才知道...嗯嗯..
下面我们来处理一下进度条的动画吧!
public class MainActivity extends AppCompatActivity {
private RoundView mRoundView;
private EditText mEditText;
private Integer mCurrentNum;
private int mMaxNum;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRoundView = findViewById(R.id.RoundView);
mEditText = findViewById(R.id.et);
}
public void click(View view) {
mMaxNum = mRoundView.getMaxNum();//获取当前最大刻度
String text = mEditText.getText().toString().trim();
if (!TextUtils.isEmpty(text)) {
mCurrentNum = Integer.decode(text);
//设置小球动画Holder
PropertyValuesHolder currentNumValuesHolder = PropertyValuesHolder.ofInt
("currentNum", 0, mCurrentNum);
int currentColor = getCurrentRgb(mCurrentNum);//获取当前设置的刻度最终的颜色
//设置背景动画Holder
PropertyValuesHolder backgroundColorValuesHolder = PropertyValuesHolder.ofObject
("BackgroundColor", new ArgbEvaluator(), Color.parseColor("#e55e10"),
currentColor);
ValueAnimator animator = ObjectAnimator.ofPropertyValuesHolder(mRoundView,
currentNumValuesHolder, backgroundColorValuesHolder);
animator.setDuration(3000);
animator.start();
}
}
//获取当前设置的结束颜色
private int getCurrentRgb(int currentNum) {
ArgbEvaluator evealuator = new ArgbEvaluator();
float fraction;
int color;
fraction = (float) currentNum / (mMaxNum);
color = (int) evealuator.evaluate(fraction, Color.parseColor("#e55e10"), Color.BLUE);//由橙到蓝
return color;
}
}
我们给自定义控件一个getMaxNum方法,用来得到在布局中设置的最大刻度
public int getMaxNum() {
return mMaxNum;
}
以及给自定义控件设置一个set,get currentNum的方法,以供属性动画使用
public void setCurrentNum(int currentNum) {
this.currentNum = currentNum;
this.invalidate();
}
public int getCurrentNum() {
return currentNum;
}
然后获取输入框中设置的当前进度值,非空判断,然后使用属性动画的ObjectAnimator.ofPropertyValuesHolder方法,来把小球的动画及背景的动画都设置进去
属性动画请参考
Android 属性动画:这是一篇很详细的 属性动画 总结&攻略
Android 动画:你真的会使用插值器与估值器吗?(含详细实例教学)
PropertyValuesHolder请参考:
自定义控件三部曲之动画篇(八)——PropertyValuesHolder与Keyframe
我们这里的背景使用了两种颜色的渐变,自己定义开始起始颜色,结束颜色我们根据输入框设置的进度使用一个ArgbEvaluator来获取.大家可参考原文的方式,或者参考这一篇作者设置颜色的方法.
Android仿支付宝9.5芝麻信用分仪表盘
OK,我们看下整体效果:
源码地址:
https://github.com/walle9/RoundIndicatorView
总结:
自定义控件看似简单,但是其中涉及到的东西还是挺杂的,包括事件分发,动画等等,有时间还是画个思维导图好好梳理下.
over...