需求:同組數據各佔總數比例的可視化顯示
功能: 各個區塊可點擊接口(點擊後旋轉到該區塊,且字體變大靠下位置顯示)
餅狀圖可隨手勢旋轉接口(旋轉到某區塊字體變大靠下位置顯示)
旋轉過程中停止觸摸,會有回彈動畫指到該區塊中央
項目地址: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;//顯示文字的顏色 //get和set省略
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)+"%"); } }); }
做開發,需要腳踏實地,日積月累,願你我共勉