Android自定义控件系列案例
Posted 张科勇
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android自定义控件系列案例相关的知识,希望对你有一定的参考价值。
案例效果:
模拟器上运行有些锯齿,真机上和预期一样好
案例分析:
看效果,第一直觉肯定是android原生态控件中没有这样的控件实现这种效果,自然想到应该需要自定义控件了,没错,这就是通过自定义控件来绘制的一个圆环进度条。仔细分析发现这个效果的进度条应该由几个部分组成,首先是无进度时的浅色圆环,然后是一个随进度变化的深色圆弧,而中间部分是一个深蓝色的实心圆,最后就是显示进度百分比的文字。这几部分大部分都是图形,所以使用图形绘制技术应该可以绘制出分部分效果,然后加上进度控制部分应该心里就有底了。
技术实现:
重写onDraw()方法的目的是为了找到绘制图形的时机与场所。
需求准备Paint画笔,并设置画笔宽度,颜色,样式等,然后绘制圆环。
重置Paint画笔,并设置画笔颜色,绘制进度圆弧。
测试进度圆弧是否会随进度的变化正常工作。
重置Paint画笔,并设置画笔颜色,绘制实心圆。
计算进度百分比,绘制文本。
- 自定义View,重写构造方法,重写onDraw()方法
package com.kedi.myprogressbar; import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import android.view.View; public class MyProgressBar extends View { public MyProgressBar(Context context) { this(context, null); } public MyProgressBar(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } }有了自定义MyProgressBar,此时就可以在XML布局中先占位了,下面是XML布局文件。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <com.kedi.myprogressbar.MyProgressBar android:id="@+id/pg" android:layout_width="120dp" android:layout_height="120dp" android:padding="5dp" android:layout_centerInParent="true" > </com.kedi.myprogressbar.MyProgressBar> <SeekBar android:id="@+id/sb" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/pg" android:max="100" android:layout_margin="20dp" /> </RelativeLayout>
- 绘制浅色圆环
Paint 画笔:
|- new Paint() 创建画笔|- setColor() 设置画笔颜色|- setStyle()设置画笔样式(Paint.Style.STROKE 虚框样式,Paint.Style.FILL 实心样式)|- setStrokeWidth()设置画笔线宽|- setAntiAlias()消除锯齿或毛边|- setTextSize()设置画文本时的文本大小|- setTypeface()设置画文本时的文本字体
Canvas画布:
|- drawCircle(cx, cy, radius, paint)绘制圆环或圆。cx,cy:圆心坐标,radius:圆半径,paint:画笔。|- drawText(text, x, y, paint)绘制文本。text:文本,x,y:文本坐标,paint:画笔。|- drawArc(oval, startAngle, sweepAngle, useCenter, paint)绘制圆弧。oval:矩形区域,用来确定圆弧形状和大小。new RectF(left, top, right, bottom)确定一个矩形区域。startAngle:圆弧始端角度。sweepAngle:圆弧末端角度。paint:画笔。userCenter:设置是否显示圆弧的两边线条,false时只画圆弧没有两边,true时带两边。如下图:
public class MyProgressBar extends View { private Paint paint;// 画笔 public MyProgressBar(Context context) { this(context, null); } public MyProgressBar(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } /** * 初始化方法 */ private void init() { paint = new Paint(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } }接下来绘制浅色圆环
private int roundW;// 圆环宽 /** * 初始化方法 */ private void init() { paint = new Paint(); //初始化圆环宽,这里考虑了适配把15dp进行了对应平台的像素转换。 roundW = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, getResources().getDisplayMetrics()); } // 1.圆心(x,y)坐标值 int centerX = getWidth() / 2; int centerY = centerX; // 2.圆环半径 int radius1 = (centerX - roundW / 2); // 3.设置圆环颜色(浅色) paint.setColor(Color.parseColor("#11339ED4")); // 4.设置画笔的风格 paint.setStyle(Paint.Style.STROKE); // 5.设置画圆环的宽度 paint.setStrokeWidth(roundW); // 6.消除锯齿 paint.setAntiAlias(true); // 7.画圆环 canvas.drawCircle(centerX, centerY, radius1, paint);
package com.kedi.myprogressbar; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; public class MyProgressBar extends View { private Paint paint;// 画笔 private int roundW;// 圆环宽 public MyProgressBar(Context context) { this(context, null); } public MyProgressBar(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } /** * 初始化方法 */ private void init() { paint = new Paint(); // 初始化圆环宽,这里考虑了适配把15dp进行了对应平台的像素转换。 roundW = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, getResources().getDisplayMetrics()); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制浅色圆环 // 1.圆心(x,y)坐标值 int centerX = getWidth() / 2; int centerY = centerX; // 2.圆环半径 int radius1 = (centerX - roundW / 2); // 3.设置画大圆环颜色 paint.setColor(Color.parseColor("#11339ED4")); // 4.设置画笔的风格 paint.setStyle(Paint.Style.STROKE); // 5.设置画圆环的宽度 paint.setStrokeWidth(roundW); // 6.消除锯齿 paint.setAntiAlias(true); // 7.画圆环 canvas.drawCircle(centerX, centerY, radius1, paint); } }
- 绘制深色进度圆弧
圆弧的绘制重点在开始角度和末端角度的计算和确定,因为我们的圆弧代表进度,所以开始角度和末端角度都与进度有关,会跟着进度的变化而变化,所以首先我们的确定当前进度和最大进度。然后就可以计算始末角度了。
private int progress = 0;// 当前进度值 private int maxProgress = 100;// 最大进度值
/** * 更新进度和界面的方法 * * @param progress */ public void setProgress(int progress) { if (progress < 0) { progress = 0; } else { this.progress = progress; } invalidate(); }计算始末角度:
开始角度 0
末端角度 (float)360 * progress / (float)maxProgress
//绘制深色进度圆弧 // 1.设置圆孤的宽度 paint.setStrokeWidth(roundW); // 2.设置圆孤进度的颜色 paint.setColor(Color.parseColor("#339ED4")); // 3.定义圆弧的形状和大小区域 RectF oval = new RectF(centerX - radius1, centerY - radius1, centerX + radius1, centerY + radius1); // 4.设置空心样式 paint.setStyle(Paint.Style.STROKE); // 5.根据进度画圆弧 canvas.drawArc(oval, 0, (float)360 * progress / (float)maxProgress, false, paint);
package com.kedi.myprogressbar; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; public class MyProgressBar extends View { private Paint paint;// 画笔 private int roundW;// 圆环宽 private int progress = 20;// 当前进度值 private int maxProgress = 100;// 最大进度值 /** * 更新进度和界面的方法 * * @param progress */ public void setProgress(int progress) { if (progress < 0) { progress = 0; } else { this.progress = progress; } invalidate(); } public MyProgressBar(Context context) { this(context, null); } public MyProgressBar(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } /** * 初始化方法 */ private void init() { paint = new Paint(); roundW = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, getResources().getDisplayMetrics()); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制浅色圆环 // 1.圆心(x,y)坐标值 int centerX = getWidth() / 2; int centerY = centerX; // 2.圆环半径 int radius1 = (centerX - roundW / 2); // 3.设置画大圆环颜色 paint.setColor(Color.parseColor("#11339ED4")); // 4.设置画笔的风格 paint.setStyle(Paint.Style.STROKE); // 5.设置画圆环的宽度 paint.setStrokeWidth(roundW); // 6.消除锯齿 paint.setAntiAlias(true); // 7.画圆环 canvas.drawCircle(centerX, centerY, radius1, paint); // 绘制深色进度圆弧 // 1.设置圆孤的宽度 paint.setStrokeWidth(roundW); // 2.设置圆孤进度的颜色 paint.setColor(Color.parseColor("#339ED4")); // 3.定义圆弧的形状和大小区域界限 RectF oval = new RectF(centerX - radius1, centerY - radius1, centerX + radius1, centerY + radius1); // 4.设置空心样式 paint.setStyle(Paint.Style.STROKE); // 5.根据进度画圆弧 canvas.drawArc(oval, 0, (float) 360 * progress / (float) maxProgress, false, paint); } }
- 添加SeekBar,并通过进度变化控制进度圆弧
这块主要是通过SeekBar进度的改变来更新进度圆弧的动态改变,逻辑很简单,监听SeekBar进度改变,然后把改变的进度通过setProgress()方法传给自定义控件,触发自定义控件重新调用onDraw()方法,根据新进度值重绘圆弧。
完整代码:
<span style="font-weight: normal;"><span style="font-size:14px;">package com.kedi.myprogressbar; import android.app.Activity; import android.os.Bundle; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; public class MainActivity extends Activity { //自定义进度条控件 private MyProgressBar pg; //SeekBar控件 private SeekBar sb; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initViews(); initEvents(); } /** * 初始化View */ private void initViews(){ pg =(MyProgressBar) findViewById(R.id.pg); sb = (SeekBar) findViewById(R.id.sb); } /** * 初始化事件监听与处理 */ private void initEvents() { sb.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { @Override public void onStopTrackingTouch(SeekBar seekBar) { } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { //改变圆弧的进度,并重新绘制圆弧,主要是通过触发自定义控件的onDraw()方法达到目的 pg.setProgress(progress); } }); } }</span></span>
- 绘制深蓝色实心圆
实心圆的绘制也是通过drawCircle()方法,只是画笔的样式为FILL实心,所以比较简单。
核心代码:
// 绘制深蓝色实心圆 // 1.实心圆半径 int radius2 = centerX - roundW; // 2.实心圆颜色 paint.setColor(Color.parseColor("#336799")); // 3.设置画笔风格为实心 paint.setStyle(Paint.Style.FILL); // 4.画实心圆 canvas.drawCircle(centerX, centerY, radius2, paint);效果图:
- 绘制百分比文本
绘制文本需要知道文本大小,文本字体,文本颜色,文本x,y坐标位置以及文本内容。
文本大小我们定义个成员变量,然后在init()方法中初始化,当然可以对外提供接口方法,设置文本大小,
文本颜色使用白色,当然也可以像文本大小那样定义成员变量,然后初始化,也可对外提供接口方法,设置文本颜色
文本x,y坐标需要根据圆心坐标与文本自身的宽高进行计算。
文本内容是进度百分比,需要根据进度进行计算。(float) progress / (float) maxProgress) * 100
核心逻辑:
// 绘制百分比文本 // 1.设置无边框 paint.setStrokeWidth(0); // 2.设置字体颜色 paint.setColor(Color.WHITE); paint.setAntiAlias(true); // 3.设置字体大小,定义成员变量textSize,然后在初始化方法中赋初始值 paint.setTextSize(textSize); // 4.设置字体 paint.setTypeface(Typeface.DEFAULT_BOLD); // 5.计算进度百分比 int percent = (int) (((float) progress / (float) maxProgress) * 100); // 6.测量字体宽度 float textWidth = paint.measureText(percent + "%"); // 7.画出进度百分比文本 canvas.drawText(percent + "%", centerX - textWidth / 2, centerY + textSize / 2, paint);
完整代码:
package com.kedi.myprogressbar; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.Typeface; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; public class MyProgressBar extends View { private Paint paint;// 画笔 private int roundW;// 圆环宽 private int textSize;// 字体大小 private int progress = 0;// 当前进度值 private int maxProgress = 100;// 最大进度值 /** * 更新进度和界面的方法 * * @param progress */ public void setProgress(int progress) { if (progress < 0) { progress = 0; } else { this.progress = progress; } invalidate(); } /** * @param context */ public MyProgressBar(Context context) { this(context, null); } /** * @param context * @param attrs */ public MyProgressBar(Context context, AttributeSet attrs) { this(context, attrs, 0); } /** * @param context * @param attrs * @param defStyleAttr */ public MyProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } /** * 初始化方法 */ private void init() { paint = new Paint(); roundW = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, getResources().getDisplayMetrics()); textSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 25, getResources().getDisplayMetrics()); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制浅色圆环 // 1.圆心(x,y)坐标值 int centerX = getWidth() / 2; int centerY = centerX; // 2.圆环半径 int radius1 = (centerX - roundW / 2); // 3.设置画大圆环颜色 paint.setColor(Color.parseColor("#11339ED4")); // 4.设置画笔的风格 paint.setStyle(Paint.Style.STROKE); // 5.设置画圆环的宽度 paint.setStrokeWidth(roundW); // 6.消除锯齿 paint.setAntiAlias(true); // 7.画圆环 canvas.drawCircle(centerX, centerY, radius1, paint); // 绘制深色进度圆弧 // 1.设置圆孤的宽度 paint.setStrokeWidth(roundW); // 2.设置圆孤进度的颜色 paint.setColor(Color.parseColor("#339ED4")); // 3.定义圆弧的形状和大小区域界限 RectF oval = new RectF(centerX - radius1, centerY - radius1, centerX + radius1, centerY + radius1); // 4.设置空心样式 paint.setStyle(Paint.Style.STROKE); // 5.根据进度画圆弧 canvas.drawArc(oval, 0, (float) 360 * progress / (float) maxProgress, false, paint); // 绘制深蓝色实心圆 // 1.实心圆半径 int radius2 = centerX - roundW; // 2.实心圆颜色 paint.setColor(Color.parseColor("#336799")); // 3.设置画笔风格为实心 paint.setStyle(Paint.Style.FILL); // 4.画实心圆 canvas.drawCircle(centerX, centerY, radius2, paint); // 绘制百分比文本 // 1.设置无边框 paint.setStrokeWidth(0); // 2.设置字体颜色 paint.setColor(Color.WHITE); // 3.设置字体大小 paint.setTextSize(textSize); // 4.设置字体 paint.setTypeface(Typeface.DEFAULT_BOLD); // 5.计算进度百分比 int percent = (int) (((float) progress / (float) maxProgress) * 100); // 6.测量字体宽度 float textWidth = paint.measureText(percent + "%"); // 7.画出进度百分比文本 canvas.drawText(percent + "%", centerX - textWidth / 2, centerY + textSize / 2, paint); } }效果图:
到此实现的效果已经与我们一开始看到的效果分毫不差了。
- 自定义属性,使进度条样式可配置(扩展部分)
效果已经实现完了,但是如果我的项目中需要一个其它主题风格的进度条,那我们不得不重新Copy一份,然后修改其中的颜色值什么的,这样的自定义控件显然不够灵活和通用,如果进度条提供了对主题风格的定制接口那就灵活不少,所以接下来做为扩展部分,要做的是为进度条提供主题风格定制途径---自定义属性与Setter方法。
经分析,把与样式相关的特征都定义成成员变量,有如下几个:
//圆环相关成员变量 private int roundW;// 圆环宽 private int roundColor;//圆环颜色 //圆弧相关成员变量 private int progress;// 当前进度值 private int progressColor;//进度圆弧颜色 private int maxProgress = 100;// 最大进度值 //实心圆相关成员变量 private int circleColor;//实心圆颜色 //百分比文本相关成员变量 private int textColor;//字体颜色 private int textSize;// 字体大小
定义自定义属性文件values/attrs.xml,并定义成上面成员变量一一对应的自定义属性:
<?xml version="1.0" encoding="utf-8"?> <resources xmlns:android="http://schemas.android.com/apk/res/android"> <declare-styleable name="MyProgressBarSytle"> <!-- 圆环相关 --> <attr name="roundW" format="dimension" /> <attr name="roundColor" format="color" /> <!-- 圆弧相关 --> <attr name="progress" format="integer" /> <attr name="progressColor" format="color" /> <attr name="maxProgress" format="integer" /> <!-- 实心圆相关 --> <attr name="circleColor" format="color" /> <!-- 百分比文本相关 --> <attr name="textColor" format="color" /> <attr name="textSize" format="dimension" /> </declare-styleable> </resources>构造方法中获取自定义属性,然后初始化上面的那几个成员变量(将原来的值做为默认值):
public MyProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(attrs); } private void init(AttributeSet attrs) { paint = new Paint(); TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.MyProgressBarSytle); roundW = (int) typedArray.getDimension(R.styleable.MyProgressBarSytle_roundW, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, getResources().getDisplayMetrics())); roundColor = typedArray.getColor(R.styleable.MyProgressBarSytle_roundColor, Color.parseColor("#11339ED4")); progress = typedArray.getInt(R.styleable.MyProgressBarSytle_progress, 0); progressColor = typedArray.getColor(R.styleable.MyProgressBarSytle_progressColor, Color.parseColor("#339ED4")); circleColor = typedArray.getColor(R.styleable.MyProgressBarSytle_circleColor, Color.parseColor("#336799")); textColor = typedArray.getColor(R.styleable.MyProgressBarSytle_textColor, Color.parseColor("#ffffff")); textSize = (int) typedArray.getDimension(R.styleable.MyProgressBarSytle_textSize, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 25, getResources().getDisplayMetrics())); typedArray.recycle(); }
将绘制逻辑各分部中样式原来的值,换成成员变量:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制浅色圆环 // 1.圆心(x,y)坐标值 int centerX = getWidth() / 2; int centerY = centerX; // 2.圆环半径 int radius1 = (centerX - roundW / 2); // 3.设置画大圆环颜色 paint.setColor(roundColor); // 4.设置画笔的风格 paint.setStyle(Paint.Style.STROKE); // 5.设置画圆环的宽度 paint.setStrokeWidth(roundW); // 6.消除锯齿 paint.setAntiAlias(true); // 7.画圆环 canvas.drawCircle(centerX, centerY, radius1, paint); // 绘制深色进度圆弧 // 1.设置圆孤的宽度 paint.setStrokeWidth(roundW); // 2.设置圆孤进度的颜色 paint.setColor(progressColor); // 3.定义圆弧的形状和大小区域界限 RectF oval = new RectF(centerX - radius1, centerY - radius1, centerX + radius1, centerY + radius1); // 4.设置空心样式 paint.setStyle(Paint.Style.STROKE); // 5.根据进度画圆弧 canvas.drawArc(oval, 0, (float) 360 * progress / (float) maxProgress, false, paint); // 绘制深蓝色实心圆 // 1.实心圆半径 int radius2 = centerX - roundW; // 2.实心圆颜色 paint.setColor(circleColor); // 3.设置画笔风格为实心 paint.setStyle(Paint.Style.FILL); // 4.画实心圆 canvas.drawCircle(centerX, centerY, radius2, paint); // 绘制百分比文本 // 1.设置无边框 paint.setStrokeWidth(0); // 2.设置字体颜色 paint.setColor(textColor); paint.setAntiAlias(true); // 3.设置字体大小 paint.setTextSize(textSize); // 4.设置字体 paint.setTypeface(Typeface.DEFAULT_BOLD); // 5.计算进度百分比 int percent = (int) (((float) progress / (float) maxProgress) * 100); // 6.测量字体宽度 float textWidth = paint.measureText(percent + "%"); // 7.画出进度百分比文本 canvas.drawText(percent + "%", centerX - textWidth / 2, centerY + textSize / 2, paint); }如果我们希望通过代码也可以修改这些样式值,就对外提供Setter方法:
/** * 设置圆环宽度 * @param roundW */ public void setRoundW(int roundW) { this.roundW = roundW; } /** * 设置圆环颜色 * @param roundColor */ public void setRoundColor(int roundColor) { this.roundColor = roundColor; } /** * 设置进度圆弧颜色 * @param progressColor */ public void setProgressColor(int progressColor) { this.progressColor = progressColor; } /** * 设置最大进度值 * @param maxProgress */ public void setMaxProgress(int maxProgress) { this.maxProgress = maxProgress; } /** * 设置实心圆颜色 * @param circleColor */ public void setCircleColor(int circleColor) { this.circleColor = circleColor; } /** * 设置百分比进度文本颜色 * @param textColor */ public void setTextColor(int textColor) { this.textColor = textColor; } /** * 设置百分比进度文本字体大小 * @param textSize */ public void setTextSize(int textSize) { this.textSize = textSize; } /** * 更新进度和界面的方法 * * @param progress */ public void setProgress(int progress) { if (progress < 0) { progress = 0; } else { this.progress = progress; } invalidate(); }最后演示一下使用自定义属性定制主题的方式:
(1)在根布局中添加命名空间
xmlns:my="http://schemas.android.com/apk/res-auto"
(2)使用自定义属性并指定属性值
完整布局代码:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:my="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" > <com.kedi.myprogressbar.MyProgressBar android:id="@+id/pg" android:layout_width="120dp" android:layout_height="120dp" android:layout_centerInParent="true" android:padding="5dp" my:circleColor="#ACD900" my:maxProgress="100" my:progressColor="#FF6400" my:roundColor="#316900" my:roundW="8dp" my:textColor="#EE3400" my:textSize="20sp" > </com.kedi.myprogressbar.MyProgressBar> <SeekBar android:id="@+id/sb" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/pg" android:layout_margin="20dp" android:max="100" /> </RelativeLayout>效果图:
到此就完成了比较灵活的圆形进度条的制作。
以上是关于Android自定义控件系列案例的主要内容,如果未能解决你的问题,请参考以下文章