Android技术知识Android开发经验谈半栈工程师

照虎画猫仿支付宝信用仪表盘

2018-05-31  本文已影响141人  walle9

这几天闲来无事,决定好好复习一下自定义控件
然后再抄一个自定义控件
仿支付宝信用仪表盘


我知道没有效果图你们是不会往下看的.

1527762757692GIFd.gif

开始照虎画猫:
新建一个类继承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>
a2805355-d697-46fc-aab0-5a0d7a54b4bb.png

这里我们自定义控件设置了宽高同时又设置了权重,这样下面的控件刚好包裹住,剩下的都分配给了自定义控件


初始化画笔:

    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);
    }

效果如下:


3cd4230a-a5fc-42a5-8172-58430e2159d1.png

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设置绘制坐标的起点为中间,这样我们就不用去测量文字的大小了.


看下效果:


2b8317d4-99cc-4d5a-b2fa-166886159c1f.jpg

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();
    }

看下效果:


c8a77b28-7ad4-430e-87f4-92c13e28d39b.png

最后我们来画信用值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();
    }

看下效果:


7bce9f35-ed6a-4fa9-8f7e-33e1eee3d35c.jpg

发现好多大神都把自定义控件和动画放在一起写博客,现在才知道...嗯嗯..

下面我们来处理一下进度条的动画吧!

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,我们看下整体效果:


1527762757692GIFd.gif

源码地址:
https://github.com/walle9/RoundIndicatorView

总结:
自定义控件看似简单,但是其中涉及到的东西还是挺杂的,包括事件分发,动画等等,有时间还是画个思维导图好好梳理下.


over...

上一篇下一篇

猜你喜欢

热点阅读