android自定义View(带旋转动画的饼状图)

需求:同组数据各占总数比例的可视化显示


功能:  各个区块可点击接口(点击后旋转到该区块,且字体变大靠下位置显示

             饼状图可随手势旋转接口(旋转到某区块字体变大靠下位置显示

             旋转过程中停止触摸,会有回弹动画指到该区块中央


项目地址:https://github.com/AndroidCloud/PieRotateView   如有不足,欢迎各位issues和开支优化

GitHub地址:https://github.com/AndroidCloud


最终实现效果:

                                                       


技术路线(简要技术思路,具体实现详见GitHub的Demo):

       1,先将数据和该View的属性封装到PieRotateBean对象中

           public class PieRotateBean {
           private List<String> list_names;//区块名称
            private List<Float> list_numbers;//数据集合
            private List<Float> list_degrees;//各个数据对应的占饼状图的度数
            private List<Integer> list_colors;//各个数据区块对应的颜色
            private boolean isShowTextonMove;//旋转的时候是否显示文字
            private boolean isShowOutSide;//是否显示灰色外圈
            private int TextColor;//显示文字的颜色
            //getset省略
 

       2,View大小适配,先根据View的宽度设置合适比例算出View的高度

           

           @Override
           protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
           int width=MeasureSpec.getSize(widthMeasureSpec);
           int height = (int) (width/5f*3.5f);
           setMeasuredDimension(width, height);
           }

           再根据View的高度上下留出部分空余(此处为高度的1/11.5),得到饼状图的半径,然后基于该半径设置字                    体的绘制位置和字体的大小,及中间总数圆形和下方三角指示的大小(大致如图所示)

                                          

      public void drawOutSide(Canvas canvas){
      float multiple=11.5f;
      Textradius= (center_y-(center_y/multiple)) * 2.8f / 4f;
      textPaint.setTextSize((center_y-(center_y/multiple))/7.6f);
      left_x=(getWidth()-(getHeight()-(center_y/multiple*2f)))/2f;
      canvas.drawCircle(center_x, center_y,center_y,OutSidePaint);
      if (pieRotate.getList_numbers()!=null){
        for (int i=0;i<pieRotate.getList_numbers().size();i++){
            piePaint.setColor(pieRotate.getList_colors().get(i));
            float hasDrgree=0;
            //得到之前部分已经叠加的角度
            for (int j=0;j<i;j++){
                hasDrgree=hasDrgree+pieRotate.getList_numbers().get(j) / pieRotate.g                    etMax_number() * 360f;
            }
            //画扇形
            canvas.drawArc(new RectF(left_x, center_y/multiple, getHeight()-center_y/                   multiple*2f + left_x, getHeight()-center_y/multiple),
                    hasDrgree+ rotate_degrees, pieRotate.getList_numbers().get(i) /                       pieRotate.getMax_number() * 360f, true, piePaint);
            if (selectPosition>=0){
                if (selectPosition==i){
                    textPaint.setTextSize((center_y-(center_y/multiple))/6.3f);
                    Textradius=(center_y-(center_y/multiple))*3.1f/4f;
                }else{
                    textPaint.setTextSize((center_y-(center_y/multiple))/7.6f);
                    Textradius=(center_y-(center_y/multiple))*2.8f/4f;
                }
            }
            if (flag) {
                //画扇形中的text,先得到对应的点的坐标
                float a = (hasDrgree + rotate_degrees + pieRotate.getList_numbers().g                     et(i) / pieRotate.getMax_number() * 360f / 2f + 360f) % 360f;
                PointF pf = getTextPoint(a);
                //Log.v("a==",a+"");
                canvas.drawText(Math.round(pieRotate.getList_numbers().get(i) / pieR                       otate.getMax_number() * 100) + "%", pf.x, pf.y, textPaint);
            }
            }
            }
            //中间的圆
            canvas.drawCircle(center_x, center_y, (getHeight()-2f*(center_y/multiple)                  ) / 6f, circlePaint);
           textPaint.setTextSize(getHeight() / 6f/3.8f);
           canvas.drawText("总数:", center_x, center_y - getHeight() / 6f/5.2f,textP                  aint);
           canvas.drawText(trimFloat(pieRotate.getMax_number()),center_x,center_y+ge                 tHeight() / 6f/2.2f,textPaint);
           //下方的小三角
            Path p = new Path();
           p.moveTo(getWidth() / 2f, getHeight()-(center_y/multiple) - getHeight() /                  20f);
           p.lineTo(getWidth() / 2f - getHeight() / 20f / 3f * 2f, getHeight()-(cent                  er_y/multiple));
           p.lineTo(getWidth() / 2f + getHeight() / 20f / 3f * 2f, getHeight()-(cent                  er_y/multiple));
           p.close();
           canvas.drawPath(p, circlePaint);
           }

       3,实现随手势旋转,原理是根据手指滑动的down的点和up的点,算出基于圆形旋转了多少度。有情况是转了好                 多圈的超过了360,此处进行修正,处理为0--360之间的度数

            具体实现如下图所示,以View的中心为圆点,假设从a点滑倒b点,根据三角定理算出角度A,然后让所画的扇                 形的起始角度和最终角度都同时再加上已经旋转的角度。

                       


      @Override
      public boolean onTouchEvent(MotionEvent event) {
      float x=event.getX();
      float y=event.getY();
      switch(event.getAction())
      {
        case MotionEvent.ACTION_DOWN:
            down_x=x;
            down_y=y;
            rotate_degrees1=degree(down_x, down_y);
            if (timer!=null){
                timer.cancel();
                task.cancel();
            }
                if (getDistance(down_x, down_y,center_x,center_y) <= getHeight() / 2                                            f) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }else{
                    getParent().requestDisallowInterceptTouchEvent(false);
                    return false;
            }
            break;
            case MotionEvent.ACTION_MOVE:
            move_x=x;
            move_y=y;
            rotate_degrees2=degree(move_x,move_y);
            rotate_degrees=hasrotate_degrees+(rotate_degrees2-rotate_degrees1);
            rotate_degrees=(rotate_degrees)%360f;
            if (rotate_degrees<0){
                rotate_degrees=rotate_degrees+360f;
            }
            //得到当前所指的扇形区域
             getSelectPosition(true);
            invalidate();
            break;
            case MotionEvent.ACTION_UP:
            if (Math.abs(x-down_x)<=15f&&Math.abs(y-down_y)<=15f){
                getSelectPosition(down_x,down_y);
                RotateForClick();
            }else{
                hasrotate_degrees = hasrotate_degrees + (rotate_degrees2 - rotate_de                                           grees1);
            hasrotate_degrees=(hasrotate_degrees)%360f;
            if (hasrotate_degrees<0){
                hasrotate_degrees=hasrotate_degrees+360f;
                }
            //需要动画旋转的角度
             getSelectPosition(false);
            RotateForMove();
            }
                break;
            }
            return true;
            }


       4,确定Text文字的绘制位置,首先Text的绘制位置为每块区域的中角线上。根据对应角度算出Text的绘制坐标

/** 获得对应角度的文字的坐标*/  /** 传入角度需提前修正,必须大于0且小于360*/ public PointF getTextPoint(float degree) {
    PointF p=new PointF();
    if (degree<0){
        degree=degree+360f;
    }
    if (degree%90==0){
        switch ((int)degree){
            case 0:
            case 360:
                 p.x=center_x+Textradius;
                 p.y=center_y;
                break;
            case 90:
                p.x=center_x;
                p.y=center_y+Textradius;
                break;
            case 180:
                p.x=center_x-Textradius;
                p.y=center_y;
                break;
            case 270:
                p.x=center_x;
                p.y=center_y-Textradius;
                break;
        }
    }else{
        switch ((int)degree/90){
            //第一象限内
            case 0:
                p.x=center_x+(Textradius*(float)Math.cos(Math.toRadians(degree)));
                p.y=center_y+(Textradius*(float) Math.sin(Math.toRadians(degree)));
                break;
            //第二象限内
            case 1:
                p.x=center_x-(Textradius*(float)Math.sin(Math.toRadians(degree-90)));
                p.y=center_y+(Textradius*(float)Math.cos(Math.toRadians(degree-90)));
                break;
            //第三象限内
            case 2:
                p.x=center_x-(Textradius*(float)Math.cos(Math.toRadians(degree-180)));
                p.y=center_y-(Textradius*(float)Math.sin(Math.toRadians(degree-180)));
                break;
            //第四象限内
            case 3:
                p.x=center_x+Textradius*(float)Math.sin(Math.toRadians(degree-270));
                p.y=center_y-Textradius*(float)Math.cos(Math.toRadians(degree-270));
                break;
        }
    }
    return p;
}

       5,点击和旋转停止的回弹动画。根据中角线偏离下方三角指示的度数来确定需要动画旋转的度数。再开启线                        程,按每隔2ms或者3ms旋转该度数的1/100,达到动画的效果,直到旋转完毕,结束Timer。

//滑动停止旋转动画
public void RotateForMove(){
    i=0;
    average_degree = (needrotateDegree-90f)/100f;
    timer = new Timer();
        task = new TimerTask() {
            @Override
            public void run() {
                handler.sendEmptyMessage(10);
            }
        };
    timer.schedule(task, new Date(), 2);
}
//点击旋转动画
public void RotateForClick(){
    i=0;
    if (onclickrotateDegree>270f){
        onclickrotateDegree=onclickrotateDegree-360f;
    }
        average_degree = (onclickrotateDegree-90f)/100f;
    timer = new Timer();
    task = new TimerTask() {
        @Override
        public void run() {
            handler.sendEmptyMessage(20);
        }
    };
    timer.schedule(task, new Date(), 3);
}
//旋转动画的实现
private Handler handler=new Handler(){
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        if (msg.what==10){
            if (i<100){
                rotate_degrees=rotate_degrees-average_degree;
                hasrotate_degrees=hasrotate_degrees-average_degree;
            invalidate();
            }
            i++;
            if (i==100){
                flag=true;
                timer.cancel();
                task.cancel();
            }
        }else if (msg.what==20){
            if (i<100){
                rotate_degrees=rotate_degrees-average_degree;
                hasrotate_degrees=hasrotate_degrees-average_degree;
                invalidate();
            }
            i++;
            if (i==100){
                flag=true;
                timer.cancel();
                task.cancel();
            }
        }
    }
};

       6,点击某一区块接口和旋转到某一区块接口。

             首先,点击时,根据点击的坐标,算出点击点与X轴正方向的夹角,根据夹角判断所点击的位置在哪个区块                      内,然后旋转到该区块,并实现接口事件。

             其次,滑动时,随时判断各个区块的中角线对应的角度与下方三角指示的夹角是否在某一区块内,如果                            是,则实现接口事件。

//事件接口
public interface onSelectionListener{
    void onSelect(int id);
}

/**得到当前所指的扇形区域和需要动画旋转的角度*/ public void getSelectPosition(float x, float y){
    if (pieRotate!=null){
        flag=pieRotate.isShowTextonMove();
        onclickrotateDegree=0;
        if (pieRotate.getList_numbers()!=null) {
            for (int i = 0; i < pieRotate.getList_numbers().size(); i++) {
                float hasDrgree = 0;
                //得到之前部分已经叠加的角度
                for (int j = 0; j < i; j++) {
                    hasDrgree = hasDrgree + pieRotate.getList_numbers().get(j) / pie              Rotate.getMax_number() * 360f;
                }
                float a = pieRotate.getList_numbers().get(i) / pieRotate.getMax_numb                er() * 360f / 2f;
                float b = (hasDrgree + rotate_degrees + pieRotate.getList_numbers().                 get(i) / pieRotate.getMax_number() * 360f / 2f + 360f) % 360f;
                PointF pointF=getTextPoint(b);
                float line_c=getDistance(x, y, center_x, center_y);
                float line_b=getDistance(pointF.x,pointF.y,center_x,center_y);
                float line_a=getDistance(x,y,pointF.x,pointF.y);
                float cosA=(line_b*line_b+line_c*line_c-line_a*line_a)/(2*line_b*lin                  e_c);
                float degree= (float) Math.toDegrees(Math.acos(cosA));
                if (degree<=a){
                    selectPosition=i;
                    onclickrotateDegree=b;
                    onSelectionListener.onSelect(selectPosition);
                    break;
                }
            }
        }
    }
}

public void getSelectPosition(boolean isMove){
    if (pieRotate!=null){
        needrotateDegree=0;
        flag=pieRotate.isShowTextonMove();
        if (pieRotate.getList_numbers()!=null){
            for (int i=0;i<pieRotate.getList_numbers().size();i++){
                float hasDrgree=0;
                //得到之前部分已经叠加的角度
                for (int j=0;j<i;j++){
                    hasDrgree=hasDrgree+pieRotate.getList_numbers().get(j) / pieRota               te.getMax_number() * 360f;
                }
                float a=pieRotate.getList_numbers().get(i) / pieRotate.getMax_number                () * 360f / 2f;
                float b=(hasDrgree + rotate_degrees + pieRotate.getList_numbers().ge                t(i) / pieRotate.getMax_number() * 360f / 2f+360f) % 360f;
                if(b>=270f){
                    if (b+a-360f-90f>0){
                        if (isMove){
                    selectPosition=i;
                            onSelectionListener.onSelect(selectPosition);
                        }else{
                        needrotateDegree=b-360f;
                    }
                        break;
                    }
                }else{
                    if (Math.abs(b-90f)<a){
                    if (isMove){
                    selectPosition=i;
                        onSelectionListener.onSelect(selectPosition);
                    }else{
                        needrotateDegree=b;
                    }
                        break;
                }
                }
            }
        }
    }
}

       7,在Activity中使用。

<com.example.vmmet.mypierotate.view.PieRotateView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="20dp"
    android:layout_marginLeft="3dp"
    android:layout_marginRight="3dp"
    android:id="@+id/pierotate1"
    />
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_test1);
    pierotateview1=(PieRotateView)findViewById(R.id.pierotate1)
    setPieRotate1(true,pierotateview1,tv1);
}
 
//数据初始化
private void setPieRotate1(boolean IsShowTextonMove,PieRotateView pieRotateView,final TextView tv) {
    final PieRotateBean pieRotate=new PieRotateBean();
    List<String> list_names=new ArrayList<>();
    list_names.add("1号机组");
    list_names.add("2号机组");
    list_names.add("3号机组");
    list_names.add("4号机组");
    list_names.add("5号机组");
    final List<Float> list_numbers=new ArrayList<>();
    list_numbers.add(100f);
    list_numbers.add(200f);
    list_numbers.add(300f);
    list_numbers.add(400f);
    list_numbers.add(400f);
    final List<Integer> list_colors=new ArrayList<>();
    list_colors.add(Color.parseColor("#FF7F00"));
    list_colors.add(Color.parseColor("#EE7AE9"));
    list_colors.add(Color.parseColor("#CD0000"));
    list_colors.add(Color.parseColor("#228B22"));
    list_colors.add(Color.parseColor("#1C86EE"));
    pieRotate.setList_colors(list_colors);
    pieRotate.setList_names(list_names);
    pieRotate.setList_numbers(list_numbers);
    pieRotate.setMax_number(1400f);
    pieRotate.setTextColor(Color.WHITE);
    pieRotate.setIsShowTextonMove(IsShowTextonMove);
    pieRotate.setIsShowOutSide(false);
    pieRotateView.setPieRotate(pieRotate);
    pieRotateView.setOnSelectionListener(new PieRotateView.onSelectionListener() {
        @Override
        public void onSelect(int id) {
            tv.setTextColor(list_colors.get(id));
            tv.setText("id="+id+" 数值="+list_numbers.get(id)+
                    " 所占百分比="+(list_numbers.get(id)/pieRotate.getMax_number()*10          0)+"%");
        }
    });

}


做开发,需要脚踏实地,日积月累,愿你我共勉