Android 自定义控件教程:绘制一个会动的能力分布图
程序员文章站
2022-04-11 22:02:14
Android 自定义控件教程:绘制一个会动的能力分布图。
一、准备工作
新建类,继承View,修改构造方法为:
public AbilityView(Conte...
Android 自定义控件教程:绘制一个会动的能力分布图。
一、准备工作新建类,继承View,修改构造方法为:
public AbilityView(Context context) { this(context, null); } public AbilityView(Context context, @Nullable AttributeSet attrs) { this(context, attrs,0); } public AbilityView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context,attrs); }
即一参和二参方法都调用三参的方法。
二、初始化全局变量:
//数据 private AbilityBean data; //角的个数 private int n; //顶点到中心点的距离 private float radius; //半径分为几段 private int intervalCount; //间隔的角度 private float angle; //画线的笔 private Paint linePaint; //画文字的笔 private Paint textPaint; //存储多边形顶点数组的数组 private List> pointsArrayList; //存储能力点的数组 private ArrayList abilityPoints; //控制变化的参数 private static int count = 0 ; private int times = 40; private int intervalTime = 25; //计时线程池 private ScheduledExecutorService scheduledExecutorService; private MyHandler myHandler;
初始化方法:
private void init(Context context, AttributeSet attrs){ // 获取自定义的属性 TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AbilityView); n = typedArray.getInteger(R.styleable.AbilityView_corner,7); radius = typedArray.getDimension(R.styleable.AbilityView_radius,dp2px(context,100)); intervalCount = typedArray.getInteger(R.styleable.AbilityView_intervalCount,4); typedArray.recycle(); // 算出间隔角度 angle = (float) ((2 * Math.PI)/n); // 初始化集合存储各个顶点 pointsArrayList = new ArrayList<>(); float x; float y; for (int i = 0; i < intervalCount; i++) { ListpointFList = new ArrayList<>(); float r = radius * ((float)(intervalCount-i)/intervalCount); for (int j = 0; j < n; j++) { // 此处减去π/2是为了让点逆时针旋转90度(为了让图是立着的 更加美观) x = (float) (r * Math.cos(j * angle - Math.PI/2)); y = (float) (r * Math.sin(j * angle - Math.PI/2)); pointFList.add(new PointF(x,y)); } pointsArrayList.add(pointFList); } abilityPoints = new ArrayList<>(); // 初始化画笔 linePaint = new Paint(Paint.ANTI_ALIAS_FLAG); linePaint.setStrokeWidth(dp2px(context,1f)); textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); textPaint.setTextSize(sp2px(context,14)); textPaint.setTextAlign(Paint.Align.CENTER); textPaint.setColor(Color.BLACK); // 初始化线程池 用于动画变动 scheduledExecutorService = Executors.newScheduledThreadPool(1); myHandler = new MyHandler(this); }
在各个部分都给出了注释。
另外还有Handler的类:
private static class MyHandler extends Handler{ WeakReference weakReference; public MyHandler(View view){ weakReference = new WeakReference<>(view); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); count++; weakReference.get().invalidate(); } }
注意内存泄露的隐患
三、绘制@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 将画布移动到中心 canvas.translate(getWidth()/2,getHeight()/2); // 画每个面 drawPolygon(canvas); // 勾勒外围轮廓线 drawOutLine(canvas); // 绘制文本 drawText(canvas); // 画出能力值的线 drawAbility(canvas); }
private void drawPolygon(Canvas canvas) { canvas.save(); linePaint.setStyle(Paint.Style.FILL_AND_STROKE); Path path = new Path(); for (int i = 0; i < intervalCount; i++) { switch (i) { case 0: linePaint.setColor(Color.parseColor("#D4F0F3")); break; case 1: linePaint.setColor(Color.parseColor("#99DCE2")); break; case 2: linePaint.setColor(Color.parseColor("#56C1C7")); break; case 3: linePaint.setColor(Color.parseColor("#278891")); break; default: } for (int j = 0; j < n; j++) { float x = pointsArrayList.get(i).get(j).x; float y = pointsArrayList.get(i).get(j).y; if (j == 0) { path.moveTo(x,y); }else{ path.lineTo(x,y); } } path.close(); canvas.drawPath(path,linePaint); path.reset(); } canvas.restore(); } private void drawOutLine(Canvas canvas) { canvas.save(); Path path = new Path(); linePaint.setColor(Color.parseColor("#99DCE2")); linePaint.setStyle(Paint.Style.STROKE); for (int i = 0; i < n; i++) { float x = pointsArrayList.get(0).get(i).x; float y = pointsArrayList.get(0).get(i).y; if (i==0) { path.moveTo(x,y); }else{ path.lineTo(x,y); } canvas.drawLine(0,0,x,y,linePaint); } path.close(); canvas.drawPath(path,linePaint); canvas.restore(); } private void drawText(Canvas canvas) { canvas.save(); Paint pointPaint = new Paint(); pointPaint.setStrokeWidth(dp2px(getContext(),2f)); pointPaint.setStyle(Paint.Style.STROKE); float r = radius + dp2px(getContext(), 15f); Paint.FontMetrics metrics = textPaint.getFontMetrics(); float textFont = (metrics.descent + metrics.ascent) / 2f; for (int i = 0; i < n; i++) { float x = (float) (r * Math.cos(angle * i - Math.PI/2)); float y = (float) (r * Math.sin(angle * i - Math.PI/2)) - textFont ; canvas.drawText(AbilityBean.ABILITY[i],x,y,textPaint); } canvas.restore(); }
比较简单,按着顺序一步一步画下来就可以了
效果图:
最后一步画出
private void drawAbility(final Canvas canvas) { if (data == null) { return; } linePaint.setColor(Color.parseColor("#E96153")); linePaint.setStyle(Paint.Style.STROKE); linePaint.setStrokeWidth(dp2px(getContext(),2f)); // 获取数据的点 int[] abilityDataArray = data.getAbilityDataArray(); for (int j = 0; j < n; j++) { float percent = abilityDataArray[j] / 100f; float x = pointsArrayList.get(0).get(j).x * percent; float y = pointsArrayList.get(0).get(j).y * percent; abilityPoints.add(new PointF(x,y)); } // 画本次的一圈 Path path = new Path(); for (int j = 0; j < n; j++) { float x = (count * abilityPoints.get(j).x)/times; float y = (count * abilityPoints.get(j).y)/times; if (j==0) { path.moveTo(x,y); } else { path.lineTo(x,y); } } path.close(); canvas.drawPath(path,linePaint); path.reset(); }
将每次要化的线分为times(属性)次,间隔为intervalTime(属性) ms。通过多次绘制来达到动画效果。
提供给外部的接口:
public void setData(AbilityBean data) { if (data == null) { return; } // count 未重置之前 跳过本次调用 if( count == 0 && !data.equalsAbility(this.data)){ this.data = data; if (scheduledExecutorService.isShutdown()) { scheduledExecutorService = Executors.newScheduledThreadPool(1); } count = 0; scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { if(count < times){ Message message = new Message(); myHandler.sendMessage(message); } else { scheduledExecutorService.shutdown(); count = 0; abilityPoints.clear(); } } }, intervalTime, intervalTime, TimeUnit.MILLISECONDS); } }
每次外部调用这个方法 传入数据 就开始绘制了。
另外加入了一些限制,为了让每次动画能动完之后再接收下一次的数据开始下一次动画。当然在实际使用中只需要展示一次的话并不需要作这个限制了。
实际效果图:
好了!大功告成!
整个操作流程就是调用abilityMapView.setData();传入数据,然后根据设置好的间隔和次数来draw;
附:
两个dp sp px转换的方法:
private int dp2px(Context context, float f){ final float scale = context.getResources().getDisplayMetrics().density; return (int) (f * scale + 0.5f); } private int sp2px(Context context, float f){ final float scale = context.getResources().getDisplayMetrics().scaledDensity; return (int) (f * scale + 0.5f); }