Android 优雅实现旋转加载控件(带自定义属性)

Android自带的进度条往往不能满足我们开发上的需求,最重要的理由是颜值不太够。

我需要一个旋转的加载控件,要看上去简单,优雅不妖艳不做作。现有自带的Android控件并不能满足我的要求,这时候就需要用到自定义View来解决了。
自定义View有两种方法一种是继承并重写View,一种是继承重写现有的控件(如:TextView),我选择了前者来做。

别的不多说直接上效果图,看到这里图中要是不是你的菜你就可以不用看下去了。


加载进度条.gif

十分的简洁,实现起来也不会非常的复杂。
控件只要分两个部分一个是不停在旋转的4分之一的圈圈,另一个是数字进度显示。

Android自带的画布已经有接口可以满足上面的两个部分的绘制,一个用圆弧绘制drawArc(),一个绘制文字drawText()。
这个控件很简单,所以功能上我们不需要太多的点缀,我们在个性化上加一点点的小心思,让它在用起来更方便,比如:线的粗细,中间字的大小,字体的颜色,线的颜色等。
把想到的这些自定义属性加到attrs.xml文件里面去



    
        
        
        
        
 

我们在弄清楚实现的思路后,我们可以直接重写一个View来做。继承了View重写它的第二个构造方法

public class RoundProgress extends View {
      public RoundProgress(Context context, @Nullable AttributeSet attrs){
        super(context, attrs);
      }
}

我们在构造方法中通过获取它的自定义属性

TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.RoundProgress);
progress_color = arr.getColor(R.styleable.RoundProgress_progress_color, Color.BLUE);
progress_weight = arr.getDimensionPixelSize(R.styleable.RoundProgress_progress_line_weight,8);
text_size= arr.getDimensionPixelSize(R.styleable.RoundProgress_progress_text_size,16);
text_color = arr.getColor(R.styleable.RoundProgress_progress_text_color,Color.BLACK);
arr.recycle();

注意获取完属性后我们需要记得调用arr的recycle方法进行回收,至于为什么要回收就是另一个知识点了。

然后我们需要初始化我们的画笔

mPaint = new Paint();
mPaint.setAntiAlias(true);

之后下一步我们需要让这个自定义View具有响应match_parent属性的效果,这一步我们需要重写onMeasure方法:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        width  = measureDimension(75,widthMeasureSpec);
        height  = measureDimension(75,heightMeasureSpec);
        setMeasuredDimension(width, height);
    }

    public int measureDimension(int defaultSize, int measureSpec){
        int result;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if(specMode == MeasureSpec.EXACTLY){
            result = specSize;
        }else{
            result = defaultSize;   //UNSPECIFIED
            if(specMode == MeasureSpec.AT_MOST){
                result = Math.max(result, specSize);
            }
        }
        return result;
    }

长宽的测量做好了,剩下就是绘制的事了,我们根据之前的分析,绘制我们控件的两个部分:

 @Override
    public void onDraw(Canvas canvas) {
        //根据oval这个矩形绘制一个圆弧
        if(oval == null){
            int shorter = getWidth()>=getHeight()?getHeight():getWidth();
            oval = new RectF( (getWidth()-shorter+16)/2, (getHeight()-shorter+16)/2,
                    (getWidth()+shorter-16)/2, (getHeight()+shorter-16)/2);
        }
      //根据自定义属性初始化画笔
        mPaint.setColor(progress_color);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(progress_weight);
      //绘制圆弧,圆弧的角度我们给了120,如果可以这个角度我们也能做成自定义属性来控制
        canvas.drawArc(oval, startPosition,  120, false, mPaint);

      //根据自定义属性初始化绘制文字的画笔
        mPaint.setTextSize(text_size);
        mPaint.setStrokeWidth(2);
        mPaint.setColor(text_color);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setTextAlign(Paint.Align.LEFT);
        mPaint.setLinearText(true);
      //计算字体绘制的位置
        float left_add_rate =(float) (progress+"%").length()/2;
        canvas.drawText(progress + "%", (getWidth()-(float)text_size*left_add_rate) / 2,  (getHeight()+(float)text_size/2) / 2, mPaint);
        //绘制完成后起始点增长,用于下一次的绘制
        startPosition += 6;
        if(startPosition >=360) {
            startPosition = 0;
        }
    }

一次的绘制就这样完成了,一个View要不停的动那么我们就需要不停的重新绘制这个 View, 那么我们需要如何不停地绘制这个View:
我们可以使用一个不停循环的线程,然后不停的发送Message给Handler通知UI线程进行View的绘制。
也可以单纯地使用Handler不停地给自己发Message,循环绘制。
我为了方便选择了后者,理论来说前者的显示效果会好一点,毕竟延时的操作不是在UI线程进行的。

后者的实现方式:
Handler mhandler=new Handler(){
        public void handleMessage(android.os.Message msg) {
            switch (msg.what) {
                case 0x001:
                    //重新绘制界面
                    invalidate();//告诉UI主线程重新绘制
                    //延迟25毫秒后发送通知
                    mhandler.sendEmptyMessageDelayed(0x001, 25);
                    break;

                default:
                    break;
            }
        }
    };
//然后在构造函数里面触发这个Handler
public RoundProgress(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        ...
        mhandler.sendEmptyMessageDelayed(0x001, 25);
    }

最后我们需要暴露一个修改进度数值的方法:

public void setProgress(int progress) {
        this.progress = progress;
    }

以上是RoundProgress的全部代码,Rebuild项目以后就能使用了。

总结

本文是我在学习过程的一些记录跟分享,这个自定义View还有很多优化的地方,目前实现的仅仅是很简单的效果,大家可以参考一下,比如在动画效果上,在样式属性上,线程优化等方面进行修改优化。

下面是完整代码:

public class RoundProgress extends View {

    private int progress = 0;
    private int startPosition = 0;
    private int width;
    private int height;

    private int progress_weight;
    private int progress_color;
    private int text_size;
    private int text_color;
    private Paint mPaint;
    private RectF oval;
    private boolean isRun = false;

    Handler mhandler = new Handler() {
        public void handleMessage(android.os.Message msg) {
            switch (msg.what) {
                case 0x001:
                    //重新绘制界面
                    invalidate();//告诉UI主线程重新绘制
                    mhandler.sendEmptyMessageDelayed(0x001, 25);
                    break;

                default:
                    break;
            }
        }
    };

    public RoundProgress(Context context) {
        super(context);
    }

    public RoundProgress(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.RoundProgress);
        progress_color = arr.getColor(R.styleable.RoundProgress_progress_color, Color.BLUE);
        progress_weight = arr.getDimensionPixelSize(R.styleable.RoundProgress_progress_line_weight, 8);
        text_size = arr.getDimensionPixelSize(R.styleable.RoundProgress_progress_text_size, 16);
        text_color = arr.getColor(R.styleable.RoundProgress_progress_text_color, Color.BLACK);

        arr.recycle();
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mhandler.sendEmptyMessageDelayed(0x001, 15);
    }

    public RoundProgress(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    public void onDraw(Canvas canvas) {
        if (oval == null) {
            int shorter = getWidth() >= getHeight() ? getHeight() : getWidth();
            oval = new RectF((getWidth() - shorter + 16) / 2, (getHeight() - shorter + 16) / 2,
                    (getWidth() + shorter - 16) / 2, (getHeight() + shorter - 16) / 2);
        }
        mPaint.setColor(progress_color);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(progress_weight);
        canvas.drawArc(oval, startPosition, 120, false, mPaint);

        mPaint.setTextSize(text_size);
        mPaint.setStrokeWidth(2);
        mPaint.setColor(text_color);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setTextAlign(Paint.Align.LEFT);
        mPaint.setLinearText(true);
        float left_add_rate = (float) (progress + "%").length() / 2;
        canvas.drawText(progress + "%", (getWidth() - (float) text_size * left_add_rate) / 2, (getHeight() + (float) text_size / 2) / 2, mPaint);
        startPosition += 6;
        if (startPosition >= 360) {
            startPosition = 0;
        }
    }

    public void startProgress() {
        mhandler.sendEmptyMessageDelayed(0x001, 1000);
    }


    public void setProgress(int progress) {
        this.progress = progress;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        width = measureDimension(75, widthMeasureSpec);
        height = measureDimension(75, heightMeasureSpec);

        setMeasuredDimension(width, height);
    }

    public int measureDimension(int defaultSize, int measureSpec) {
        int result;

        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        //Log.i("specMode",specMode+"");
        //Log.i("specSize",specSize+"");

        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = defaultSize;   //UNSPECIFIED
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.max(result, specSize);
            }

        }
        //Log.i("result",result+"");
        return result;
    }

}

你可能感兴趣的:(Android 优雅实现旋转加载控件(带自定义属性))