前言
最近在学习自定义控件,正好撸了一个自定义控件来练练手,巩固一下新姿势。
嗯。先贴一下参考的链接
本文参考:(http://blog.csdn.net/fu908323236/article/details/78356344)
一、准备工作
新建类,继承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<List<PointF>> pointsArrayList;
//存储能力点的数组
private ArrayList<PointF> 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++) {
List<PointF> pointFList = 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);
}
在各个部分都给出了注释。
如何设置自定义属性可以看Android 自定义控件大道(一) 自定义属性
另外还有Handler的类:
private static class MyHandler extends Handler{
WeakReference<View> 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);
}