Android 实现自定义圆环

用途说明:

这是一个自定义的圆环图像,支持动画展示,可以自定义圆环的颜色和占比,主要用以展示一些数据占比方面展示的android圆环。

圆环实现思路:

android的自定义圆环实现有很多种方法,这里只介绍我实现的思路。主要思路是先画一个大圆,然后再画一个与大圆同圆心的小圆,然后小圆的颜色可以设置为背景色,这样看上去就是一个圆环了。

实现效果:

动画效果

使用方法:

1.布局文件中直接使用自定义圆环(RingView),控件的宽和高需要固定的尺寸

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"       
       android:layout_width="match_parent"    
       android:layout_height="match_parent"    
       android:background="@android:color/white"    
       android:orientation="vertical">    

       <com.wuden.zxingproject.widgets.RingView        
                android:id="@+id/rvRingView"        
                android:layout_gravity="center"        
                android:layout_width="300dp"        
                android:layout_height="300dp" />
</LinearLayout>

2.在对应的activty中调用一些方法来实现你的需求即可

public class TestActivity extends AppCompatActivity {    
      @Bind(R.id.rvRingView)    
      RingView mRvRingView;    
      @Override    
      protected void onCreate(@Nullable Bundle savedInstanceState) {        
          super.onCreate(savedInstanceState);        
          setContentView(R.layout.layout_test1);       
          ButterKnife.bind(this);
          mRvRingView.setAnglesData("12.2","230","6799.01","1","111","200");//直接设置String类型的数据
//        mRvRingView.setAnglesData(12.2,230,6799.01,1,111,200);//直接设置double类型的数据
//        mRvRingView.setAngles(20, 40, 100, 180, 20);//设置的是角度
          
 //       mRvRingView.setRingStartAngle(-90);//设置圆环的开始角度,不设置默认是-90        
         //设置画笔的颜色,支持字符串和资源文件可变参数。          
          mRvRingView.initPaint("#123456", "#fea123", "#fefefe", "#78da10", "#1121de", "#aacc18");//支持字符串
//        mRvRingView.initPaint(R.color.color_first_part,R.color.color_second_part,
//                             R.color.color_third_part,R.color.color_fourth_part,
//                             R.color.color_fifth_part,R.color.color_sixth_part);
//        mRvRingView.setInnerCirclePaintColor("#ffffff");//内圆的画笔颜色,默认#ffffff    
          mRvRingView.setRingStrokeWidth(40);//圆环的环宽,默认20
//        mRvRingView.showViewWithAnimation(1000);//自定义动画时长展示圆环
//        mRvRingView.showViewWithoutAnimation();//展示圆环不带动画   
          mRvRingView.showViewWithAnimation();//动画展示圆环,默认2s    
      }
}

3.自定义view的源码

public class RingView extends View {    
    private static final int CIRCLE_ANGLE = 360;//圆环的角度    
    private static final int RING_STROKE_WIDTH = 20;//默认圆环的宽度为20dp    
    private Paint mNoAssetsPaint, mInnerCirclePaint;    
    private ArrayList<Paint> mPaints;    
    private int mRingStrokeWidth;//圆环的宽度    
    private int mCanvasWidth, mCanvasHeight;    
    private RectF mRingRect, mInnerRect;    
    private int mDensity;//手机屏幕密度    
    private int mNoDataPaintColor = Color.parseColor("#cccccc");//没有数据的paint的颜色    
    private int mInnerCirclePaintColor = Color.parseColor("#ffffff");//内圆的paint的颜色    
    private ArrayList<Integer> mAngles;//传入的数据    
    private boolean mHasData = false;    
    private ArrayList<Integer> mLevelStartAngles;//每段圆弧的起始角度值    
    private int mMoveAngle;//圆弧移动的角度    
    private int mRingStartAngle = -90;//圆环的起始角度    
    private RingAnimation mRingAnim;    

    public RingView(Context context) {
        super(context);
        init(context);
    }

    public RingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public RingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context ctx) {
        mDensity = (int) ctx.getResources().getDisplayMetrics().density;
        mRingStrokeWidth = RING_STROKE_WIDTH * mDensity;
        mPaints = new ArrayList<Paint>();
        mAngles = new ArrayList<Integer>();
        mLevelStartAngles = new ArrayList<Integer>();
        mNoAssetsPaint = new Paint();
        mNoAssetsPaint.setAntiAlias(true);
        mNoAssetsPaint.setStyle(Paint.Style.FILL);
        mNoAssetsPaint.setColor(mNoDataPaintColor);
        mInnerCirclePaint = new Paint();
        mInnerCirclePaint.setAntiAlias(true);
        mInnerCirclePaint.setStyle(Paint.Style.FILL);
        mInnerCirclePaint.setColor(mInnerCirclePaintColor);
        mRingAnim = new RingAnimation();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mCanvasWidth == 0) {
            initRect(); 
       } 
       if (!mHasData) {//没有数据
            mMoveAngle = CIRCLE_ANGLE; 
           drawRingView(canvas, mRingStartAngle, mMoveAngle, mNoAssetsPaint); 
       } else {
            int _level = 0;//圆弧的段数
            for (int _i = 0; _i < mAngles.size(); _i++) {//计算需要画几段圆弧
                if (mMoveAngle < mLevelStartAngles.get(1)) { 
                   _level = 1; 
               } else if (mMoveAngle > mLevelStartAngles.get(_i) && mMoveAngle <= mLevelStartAngles.get(_i + 1)) {
                   _level = _i + 1;
               }
            }
            drawRing(_level, canvas); 
       }
        canvas.drawArc(mInnerRect, mRingStartAngle, CIRCLE_ANGLE, true, mInnerCirclePaint);//画内部的圆
    }

    /**
     *
     * @param level 圆环的段数
     * @param canvas
     */
    private void drawRing(int level, Canvas canvas) {
        if (level <= 0) {
            drawRingView(canvas, mRingStartAngle, CIRCLE_ANGLE, mNoAssetsPaint);
            return;
        }
        if (mAngles.size() > mPaints.size()) {
            int _temp = mAngles.size() - mPaints.size();
            for (int _i = 0; _i < _temp; _i++) {
                mPaints.add(mNoAssetsPaint);
            }
        }
        for (int _i = 0; _i < level; _i++) {
            if (_i == level - 1) {
                drawRingView(canvas, mRingStartAngle + mLevelStartAngles.get(_i), 
mMoveAngle - mLevelStartAngles.get(_i), mPaints.get(_i));
            } else {
                drawRingView(canvas, mRingStartAngle + mLevelStartAngles.get(_i), mAngles.get(_i), mPaints.get(_i));
            }
        }
    }

    /**
     *
     * @param canvas
     * @param startAngle 开始的角度
     * @param sweepAngle 旋转的角度
     * @param paint 画笔 
     */
    private void drawRingView(Canvas canvas, int startAngle, int sweepAngle, Paint paint) {
        if (sweepAngle != 0) {
            canvas.drawArc(mRingRect, startAngle, sweepAngle, true, paint);
        }
    }

    public void setNoDataPaintColor(int color) {
        mNoAssetsPaint.setColor(getResources().getColor(color));
    }

    public void setNoDataPaintColor(String color) {
        mNoAssetsPaint.setColor(Color.parseColor(color));
    }

    public void setInnerCirclePaintColor(int colorId) {
        mInnerCirclePaint.setColor(getResources().getColor(colorId));
    }

    public void setInnerCirclePaintColor(String color){
        mInnerCirclePaint.setColor(Color.parseColor(color));
    }

    public void initPaint(ArrayList<Integer> colors) {
        mPaints.clear();
        for (int _i = 0; _i < colors.size(); _i++) {
            Paint _paint = new Paint();
            _paint.setAntiAlias(true);
            _paint.setStyle(Paint.Style.FILL);
            _paint.setColor(colors.get(_i));
            mPaints.add(_paint);
        }
    }

    public void initPaint(String... colors) {
        ArrayList<Integer> _colors = new ArrayList<Integer>();
        for (int _i = 0; _i < colors.length; _i++) {
            _colors.add(Color.parseColor(colors[_i]));
        }
        initPaint(_colors);
    }

    public void initPaint(int... colorIds) {
        ArrayList<Integer> _colors = new ArrayList<Integer>();
        for (int _i = 0; _i < colorIds.length; _i++) {
            _colors.add(getResources().getColor(colorIds[_i]));
        }
        initPaint(_colors);
    }

    private void initRect() {
        mCanvasWidth = getWidth();
        mCanvasHeight = getHeight();
        mInnerRect = new RectF(mRingStrokeWidth, mRingStrokeWidth, mCanvasWidth - mRingStrokeWidth, mCanvasHeight - mRingStrokeWidth);
        mRingRect = new RectF(0, 0, mCanvasWidth, mCanvasHeight);
    }

    /**
     * 设置圆环起始的角度
     * @param angle
     */

    public void setRingStartAngle(int angle){
        mRingStartAngle = angle;
    }

    /**
     * 设置圆环的环宽
     *
     * @param width
     */
    public void setRingStrokeWidth(int width) {
        mRingStrokeWidth = width * mDensity;
        invalidate();
    }

    /**
     * 所需要显示的数据的角度
     *
     * @param angles
     */
    public void setAngles(int... angles) {
        ArrayList<Integer> _angles = new ArrayList<Integer>();
        for (int _i = 0; _i < angles.length; _i++) {
            _angles.add(angles[_i]);
        }
        setAngles(_angles);
    }

    /**
     * 所需要显示的数据的角度
     *
     * @param angles
     */

    public void setAngles(ArrayList<Integer> angles) {
        mAngles.clear();
        mAngles.addAll(angles);
        mLevelStartAngles.clear();
        mLevelStartAngles.add(0);
        int _angle = 0;
        for (int _i = 0; _i < mAngles.size(); _i++) {
            _angle += mAngles.get(_i);
            mLevelStartAngles.add(_angle);
            if (mAngles.get(_i) > 0) {
                mHasData = true;
            }
        }
    }

    /**
     * 设置数据来计算角度并绘制圆环
     *
     * @param data
     */
    public void setAnglesData(BigDecimal... data) {
        BigDecimal _total = new BigDecimal("0.00");
        for (int _i = 0; _i < data.length; _i++) {
            _total = _total.add(data[_i]);
        }

        if (_total.compareTo(BigDecimal.valueOf(0)) == 0) {
            mHasData = false;
            return;
        }

        BigDecimal[] _dbData = new BigDecimal[data.length];
        for (int _i = 0; _i < data.length; _i++) {
            _dbData[_i] = data[_i].divide(_total, 10, ROUND_HALF_UP).multiply(BigDecimal.valueOf(360));
        }

        int[] _intData = new int[data.length];
        for (int _i = 0; _i < data.length; _i++) {
            //数值小于1且大于0的,就直接定1,否则转int类型,确保小数据也能出现在圆环上
            _intData[_i] = _dbData[_i].compareTo(BigDecimal.valueOf(1.0)) < 0 && _dbData[_i].compareTo(BigDecimal.valueOf(0)) > 0 ?
                    1 : _dbData[_i].intValue();
        }

        //所有数据加起来可能会不满360也可能会超出360,由于精度的问题
        //处理方案是把缺少的度数(有正也有负)加在最大的值上,这样图形出现的误差会不明显
        int _remind = 360;//剩余的角度
        int _maxPosition = -1, _max = _intData[0];
        for (int _i = 0; _i < _intData.length; _i++) {
            _remind = _remind - _intData[_i];
            if (_max <= _intData[_i]) {
                _maxPosition = _i;
            }
        }
        _intData[_maxPosition] += _remind;//将缺少的度数加载最大值上

        //将最终的数据设置到圆环上
        setAngles(_intData);
    }

    public void setAnglesData(String... data) {
        BigDecimal[] _bdData = new BigDecimal[data.length];
        for (int _i = 0; _i < data.length; _i++) {
            _bdData[_i] = new BigDecimal(TextUtils.isEmpty(data[_i]) ? "0" : data[_i]);
        }
        setAnglesData(_bdData);
    }

    public void setAnglesData(double... data) {
        BigDecimal[] _bdData = new BigDecimal[data.length];
        for (int _i = 0; _i < data.length; _i++) {
            _bdData[_i] = BigDecimal.valueOf(data[_i]);
        }
        setAnglesData(_bdData);
    }

    /**
     * 自定义动画时间的圆环
     *
     * @param animTime
     */
    public void showViewWithAnimation(int animTime) {
        startAnimation(animTime);
    }

    /**
     * 默认时间(2000)的圆环
     */
    public void showViewWithAnimation() {
        startAnimation(-1);
    }

    /**
     * 不带动画的圆环
     */
    public void showViewWithoutAnimation() {
        mMoveAngle = CIRCLE_ANGLE;
        invalidate();
    }

    private void startAnimation(int animTime) {
        mRingAnim.setDuration(animTime <= 0 ? 2000 : animTime);
        startAnimation(mRingAnim);
    }

    private class RingAnimation extends Animation {
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            mMoveAngle = (int) (interpolatedTime * CIRCLE_ANGLE);
            invalidate();
        }
    }
}

4.代码实现的一些注意点

1)控件的宽和高必须是固定的,不然无法显示。
2)画笔颜色的数组长度必须大于或等于数据数组的长度,不然超出的数据将由默认的没有数据的颜色显示。
3)在设置画笔颜色时,使用的字符串形式的颜色必须严格遵循颜色的书写方式,不然会出现无法正确显示view。例如:不支持“#fff”,支持“#ffffff”。

该控件中还存在很多需要优化的地方和更多的功能支持,后期会补充,希望各位大神可以给出指导性的意见和建议,十分感谢🙏。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容