Android鬼点子-自定义View就像PS

分享一个最近实现的一个效果,主要是用来显示分数。分数的范围是0~100,没有小数。

我一步一步的分解开,来说说我是怎么实现的。首先来一张动态效果。
设置分数后,分数会从0到目标分数增长,并伴随圆环的动画。

在你阅读此文之前最好先了解自定义View的步骤,比如onMeasure,onLayout,onDraw等等。这类的文章有很多,我这里不再一一赘述了。

准备阶段

一.首先建好Activity,和Activity的布局:

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) }
    }
}
复制代码

布局文件:

<?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:

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类:

自定义的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文件中的颜色。

到此准备工作结束。

实现阶段

三.测量宽高

这是一个方形的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°,所以有些数值需要四舍五入。

//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。这个角的顶点在小球的圆心和View圆心的连线上,角度是endR。另外两个点是角的顶点与小球的切点,这个就比较难了。因为这个水滴比较小,所以其实这两个点不用十分精确,我把endR分别向左右移动2.5°,然后半径从centre * 0.95f稍稍减小了一点到centre * 0.94f,差不多找到了‘切点’的位置。

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


}
复制代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值