虽然android本身给我们提供了形形色色的控件,基本能够满足日常开发的需求,但是面对日益同质化的app界面,和不同的业务需求.我们可能就需要自定义一些View来获得比较好的效果.自定义View是android开发者走向高级开发工程师必须要走的一关.
转载请标明出处:http://blog.csdn.net/unreliable_narrator/article/details/51274264
一般我们构造函数可以写成这样:
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}二.自定义命名空间:<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--属性申明必须是自定义控件的名称-->
<declare-styleable name="MyView">
<!--采用驼峰命名规则,可以随意命名,format是属性的单位-->
<attr name="roundColor" format="color"></attr>
</declare-styleable>
</resources> ( 2.)如果是有公共的属性部分,可以将属性包含在公共属性部分里面.也就是说公共属性可以被多个自定义控件属性样式使用.<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--采用驼峰命名规则,可以随意命名,format是属性的单位-->
<attr name="roundColor" format="color"></attr>
<!--属性申明必须是自定义控件的名称-->
<declare-styleable name="MyView">
<attr name="roundColor"></attr>
</declare-styleable>
</resources> 所有的format类型理论知识讲的有点多,可能有点空洞,下面通过一个小的例子,来测试一下我们的命名空间是否可以正常使用.例子:我们自定义一个View,这个View的形状是一个圆形,并且我们不希望将圆的颜色写死,可以在布局文件中进行设置.其他的我们都先不管,只是测试一下自定义命名空间.步骤1.创建一个View继承自View.并且重写它的构造函数
public class MyView extends View {
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
} 步骤2:在Values文件夹下面创建一个attrs的文件,写上自定义的属性.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--属性申明必须是自定义控件的名称-->
<declare-styleable name="MyView">
<!--采用驼峰命名规则,可以随意命名,format是属性的单位-->
<attr name="roundColor" format="color"></attr>
<attr name="radius" format="dimension"></attr>
</declare-styleable>
</resources>步骤3:在布局文件中进行引用自定义命名空间.这里给设置的颜色是橘黄色.<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.dapeng.viewdemo.MainActivity">
<com.dapeng.viewdemo.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/holo_red_light"
app:roundColor="@android:color/holo_blue_bright"
app:radius="50dp"
/>
</RelativeLayout>
步骤4:在自定义View中设置我们自定义的属性.public class MyView extends View {
private int mColor;
private Paint mP;
private float mRadius;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//拿到自定义属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyView);
mColor = typedArray.getColor(R.styleable.MyView_roundColor, 0XFF00FF00);
mRadius = typedArray.getDimension(R.styleable.MyView_radius, 50);
//回收资源
typedArray.recycle();
//创建画笔
mP = new Paint();
//设置画笔颜色
mP.setColor(mColor);
//设置抗锯齿
mP.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画圆
canvas.drawCircle(mRadius, mRadius, mRadius, mP);
}
}
Ok代码书写完毕,我们来看看实现的效果是怎么样的.当然,这里只是实现了自定义控件的一小部分功能,接着我们来看看一个问题:我们将我们自定义控件的background设置为红色来看看效果.这里控件都是设置包裹内容的.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.dapeng.viewdemo.MainActivity">
<com.dapeng.viewdemo.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/holo_red_light"
app:roundColor="@android:color/holo_blue_bright"
/>
</RelativeLayout>效果如下图所示:看到这个效果可能会有点惊奇了,明明我设置的是包裹内容的,为什么控件确实填充了父窗体?带着这样的疑问我们接下来学习,自定义控件的另外一个非常重要的函数:onmeasure();onmeuse()方法:测量自己的大小,为正式布局提供建议。(注意,只是建议,至于用不用,要看onLayout);主要的作用就是处理自定义VIewgroup的时候是wrap_content的时候该ViewGrop的大小.定义:如果layout_widht和layout_height是match_parent或具体的xxxdp,那就非常简单了,直接调用setMeasuredDimension()方 法,设置ViewGroup的宽高即可.But如果是wrap_content,就比较麻烦了,如果不重写 onMeasure() 方法,系统则会不知道该默认多 大尺寸,就会默认填充整个父布局,所以,重写 onMeasure() 方法的目的,就是为了能够给 View 一个 wrap_content 属性下的默认大 小。调用此方法会传进来的两个参数:int widthMeasureSpec, int heightMeasureSpec .他们是父类传递过来给当前view的一个建议值, 即把当前view的尺寸设置为宽widthMeasureSpec,高heightMeasureSpec 虽然表面上看起来他们是int类型的数字,其实他们是由 mode+size两部分组成的。 widthMeasureSpec和heightMeasureSpec转化成二进制数字表示,他们都是30位的。前两位代表mode(测量模 式),后面28位才是他们的实际数值(size)。MeasureSpec.getMode()获取模式MeasureSpec.getSize()获取尺寸mode的值有三种为:EXACTLY:表示设置了精确的值,一般当childView设置其宽、高为精确值(也就是我们在布局文件中设定的值如50dp)、match_parent时,ViewGroup会将其设置为EXACTLY;AT_MOST:表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,ViewGroup会将其设置为AT_MOST;
UNSPECIFIED:表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此种模式比较少见。
我们需要判断当布局文件中设置控件为包裹内容的时候,控件的大小的值就可以了.因此重写onmeasure()方法如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取测量的模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//获取测量的值
int withSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//设置控件的大小
setMeasuredDimension(widthMode == MeasureSpec.AT_MOST ? (int) mRadius * 2 : withSize, heightMode == MeasureSpec.AT_MOST ? (int) mRadius * 2 : heightSize);
} 设置成功以后,直接将工程运行起来就可以看到效果了:上面的例子都是演示的画圆,如果想画其他的形状应该怎么办呢?我们需要通过重写onDraw() 方法对控件重写进行绘制就可以了.Ondraw().draw就是画的意思从字面意思我们也可以知道.通过重写该方法我们可以对绘制出相关的控件.那么绘制的时候在是在什么上面进行绘制呢?我们 先来重写ondraw()看看:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
画圆:canvas.drawCircle()我们可以看到通过重写ondraw()会传过来一个Canvas类,这个类实际上就是一块儿画布,我们可以创建画笔在上面进行绘图.Canvas的使用.这个类相当于一个画布,你可以在里面画很多东西;
我们可以把这个Canvas理解成系统提供给我们的一块内存区域(但实际上它只是一套画图的API,真正的内存是下面的 Bitmap),而且它还提供了一整套对这个内存区域进行操作的方法,所有的这些操作都是画图API。也就是说在这种方式
下我们已经能一笔一划或者使用画笔来画我们所需要的东西了,要画什么要显示什么都由我们自己控制。这种方式根据环 境还分为两种:一种就是使用普通View的canvas画图,还有一种就是使用专门的SurfaceView的canvas来画图。两种的 主要是区别就是可以在SurfaceView中定义一个专门的线程来完成画图工作,应用程序不需要等待View的刷图,提高性 能。前面一种适合处理量比较小,帧率比较小的动画,比如说简单的View样式或者是象棋游戏之类的;而后一种主要用 在游戏,高品质动画方面的画图。
Canvas可以绘制的对象有:弧线(arcs)、填充颜色(argb和color)、 Bitmap、圆(circle和oval)、点(point)、线(line)、矩形 (Rect)、图片(Picture)、圆角矩形 (RoundRect)、文本(text)、顶点(Vertices)、路径(path)。通过组合这些对象我们可以画出一 些简单有趣的界面出来,但是光有这些功能还是不够的,如果我要画一个仪表盘(数字围绕显示在一个圆圈中)呢? 幸好Android 还提供了一些对Canvas位置转换的方法:rorate、scale、translate、skew(扭曲)等,而且它允许你通过获得它的转换矩阵对象 (getMatrix方法) 直接操作它。这些操作就像是虽然你的笔还是原来的地方画,但是画纸旋转或者移动了,所以你画的东西的方 位就产生变化。为了方便一些转换操作,Canvas 还提供了保存和回滚属性的方法(save和restore),比如你可以先保存目前画纸 的位置(save),然后旋转90度,向下移动100像素后画一些图形,画完后调用restore方法返回到刚才保存的位置.画一些比较常见的几何图形:
canvas.drawCircle(100, 100, 90, paint);
//绘制弧线区域
//先要绘制矩形
RectF rect = new RectF(0, 0, 100, 100);
canvas.drawArc(rect, //弧线所使用的矩形区域大小
270, //开始角度
90, //扫过的角度
true, //是否使用中心
paint); //画笔 canvas.drawColor(Color.BLUE);
canvas.drawLine(10,//x起点位置
10, //y起点位置
100, //x终点位置
100, //y终点位置
paint); //画笔
//定义一个矩形区域
RectF oval = new RectF(0,0,200,300);
//矩形区域内切椭圆
canvas.drawOval(oval, paint); //按照既定点 绘制文本内容
canvas.drawPosText("Android", new float[]{
10,10, //第一个字母在坐标10,10
20,20, //第二个字母在坐标20,20
30,30, //....
40,40,
50,50,
60,60,
70,70,
}, paint); RectF rect = new RectF(50, 50, 200, 200);
canvas.drawRect(rect, paint); RectF rect = new RectF(50, 50, 200, 200);
canvas.drawRoundRect(rect,
30, //x轴的半径
30, //y轴的半径
paint); Path path = new Path(); //定义一条路径
path.moveTo(10, 10); //移动到 坐标10,10
path.lineTo(50, 60);
path.lineTo(200,80);
path.lineTo(10, 10);
canvas.drawPath(path, paint); Path path = new Path(); //定义一条路径
path.moveTo(10, 10); //移动到 坐标10,10
path.lineTo(50, 60);
path.lineTo(200,80);
path.lineTo(10, 10);
canvas.drawTextOnPath("Android", path, 10, 10, paint);drawBitmap(Bitmap bitmap, float left, float top, Paint paint)
canvas.rotate(360 / count,//旋转的角度
0f, x轴的坐标
0f); //旋转画纸
canvas.translate(200, 200); //将位置移动画纸的坐标点到x为200,y为200
canvas.save(); //保存画布的状态
canvas.restore(); //取出保存的状态
canvas.save();和canvas.restore();是两个相互匹配出现的,作用是用来保存画布的状态和取出保存的状态的。这里稍微解释一下,
当我们对画布进行旋转,缩放,平移等操作的时候其实我们是想对特定的元素进行操作,比如图片,一个矩形等,但是当你用canvas的方法来进行这些操作的时候,其实是对整个画布进行了操作,那么之后在画布上的元素都会受到影响,所以我们在操作之前调用canvas.save()来保存画布当前的状态,当操作之后取出之前保存过的状态,这样就不会对其他的元素进行影响.
画笔Paint
从上面列举的几个Canvas.drawXxx()的方法看到,其中都有一个类型为paint的参数,可以把它理解为一个"画笔",通过这个画笔,在Canvas这张画布上作画。 它位于"android.graphics.Paint"包下,主要用于设置绘图风格,包括画笔颜色、画笔粗细、填充风格等。
Paint中提供了大量设置绘图风格的方法,这里仅列出一些常用的:
invalidate()和postInvalidate()的区别.
通过上面的讲解,我在自定义一个静态的View已经是一件非常容易的事情了,但是我们使用的自定义的View有很多是需要根据一个变量去不断绘制的,这个时候就引出了新的函数invalidate()和postinvalidate(),使用此函数可以是的ondraw()不断的去执行从而达到不断绘制的效果.
Android中实现view的更新有两组方法,一组是invalidate,另一组是postInvalidate,其中前者是在UI线程自身中使用,而后者在非UI线程中使用。
接下来通过一个稍微综合一点的例子来对自定义View做一个总结:
先来看看实现的效果:
我们先来分析一下,这个效果实际上就是外面是不断的去绘制一个扇形,然后中间盖了一个小的圆:
好了,接下来我们来讲一讲实现的步骤:
步骤一:首先定义attrs文件:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ProgressView">
<attr name="smallRoundColor" format="color"></attr>
<attr name="smallRoundRadius" format="dimension"></attr>
<attr name="arcRadius" format="dimension"></attr>
<attr name="arcColor" format="color"></attr>
<attr name="textColor" format="color"></attr>
<attr name="textSize" format="dimension"></attr>
</declare-styleable>
</resources>步骤二:编写自定义View:
public class ProgressView extends View {
private float mArcRadius;
private float mSmallRoundRadius;
private int mArcColor;
private int mSmallRoundColor;
private Paint mRoundpaint;
private Paint mArcpaint;
private float sweepAngle;
private int mTextColor;
private Paint mTextPaint;
private int mTextSize;
public ProgressView(Context context) {
this(context, null);
}
public ProgressView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ProgressView);
//扇形半径
mArcRadius = array.getDimension(R.styleable.ProgressView_arcRadius, 50);
//小圆半径
mSmallRoundRadius = array.getDimension(R.styleable.ProgressView_smallRoundRadius, 50);
//扇形颜色
mArcColor = array.getColor(R.styleable.ProgressView_arcColor, 0XFF00FF00);
//小圆颜色
mSmallRoundColor = array.getColor(R.styleable.ProgressView_smallRoundColor, 0XFF00FF00);
//百分比字体颜色
mTextColor = array.getColor(R.styleable.ProgressView_textColor, 0XFF00FF00);
//百分比字体大小
mTextSize = array.getDimensionPixelSize(R.styleable.ProgressView_textSize, 15);
//释放资源
array.recycle();
//画圆的画笔
mRoundpaint = new Paint();
mRoundpaint.setColor(mSmallRoundColor);
mRoundpaint.setAntiAlias(true);
//画扇形的画笔
mArcpaint = new Paint();
mArcpaint.setColor(mArcColor);
mArcpaint.setAntiAlias(true);
//绘制中间文字部分的画笔文本
mTextPaint = new Paint();
mTextPaint.setColor(mTextColor);
mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(mTextSize);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//设置View的大小
int withMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int withSize = MeasureSpec.getSize(widthMeasureSpec);
int heightsize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(withMode == MeasureSpec.AT_MOST ? (int) mArcRadius * 2 : withSize, heightMode == MeasureSpec.AT_MOST ? (int) mArcRadius * 2 : heightsize);
}
@Override
protected void onDraw(Canvas canvas) {
//画圆弧
RectF rect = new RectF(0, 0, (int) mArcRadius * 2, (int) mArcRadius * 2);
canvas.drawArc(rect, 270, (float) (sweepAngle * 3.6), true, mArcpaint);
//画小圆
canvas.drawCircle(mArcRadius, mArcRadius, mSmallRoundRadius, mRoundpaint);
String text = (int) (sweepAngle) + "%";
float textLength = mTextPaint.measureText(text);
//把文本画在圆心居中
canvas.drawText(text, mArcRadius - textLength / 2, mArcRadius, mTextPaint);
super.onDraw(canvas);
}
//提供一个给外界的方法可以不断的去设置扇形的弧度
public void percent(float sweepAngle) {
if (sweepAngle <= 100) {
this.sweepAngle = sweepAngle;
//刷新界面
postInvalidate();
}
}
}步骤三:在布局文件中进行使用:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.dapeng.viewdemo.SecondActivity">
<com.dapeng.viewdemo.ProgressView
android:id="@+id/pv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:arcColor="@android:color/holo_blue_bright"
app:arcRadius="120dp"
app:textSize="20sp"
app:smallRoundColor="@android:color/transparent"
app:textColor="@android:color/holo_orange_light"
app:smallRoundRadius="100dp"/>
<Button
android:id="@+id/btn"
android:text="开始绘制"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
</RelativeLayout>步骤四:在需要用到的地方模拟数据去使用自定义的View.public class SecondActivity extends AppCompatActivity {
private int mTotalProgress;
private int mCurrentProgress;
private ProgressView mPv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
mPv = (ProgressView) findViewById(R.id.pv);
initVariable();
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mCurrentProgress=0;
new Thread(new ProgressRunable()).start();
}
});
}
private void initVariable() {
mTotalProgress = 100;
mCurrentProgress = 0;
}
class ProgressRunable implements Runnable {
@Override
public void run() {
while (mCurrentProgress < mTotalProgress) {
mCurrentProgress += 1;
mPv.percent((float) mCurrentProgress);
try {
Thread.sleep(50);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
至此大功告成.
原文:http://blog.csdn.net/unreliable_narrator/article/details/51274264