首页 > 代码库 > Android中自定义视图View之---开发案例
Android中自定义视图View之---开发案例
自定义视图View的案例
下面我们就是开始正式的进入自定义视图View了
在讲解正式内容之前,我们先来看一下基本知识
1、我们在自定义视图View的时候正确的步骤和方法
1)、必须定义有Context/Attrbuite参数的构造方法,并且调用父类的方法
public LabelView(Context context, AttributeSet attrs)
不然会报错:
2)、重写onMeasure方法
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); }来设置View的大小:
private int measureWidth(int measureSpec) { int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); Log.i("DEMO","measureSpec:"+Integer.toBinaryString(measureSpec)); Log.i("DEMO","specMode:"+Integer.toBinaryString(specMode)); Log.i("DEMO","specSize:"+Integer.toBinaryString(specSize)); /** * 一般来说,自定义控件都会去重写View的onMeasure方法,因为该方法指定该控件在屏幕上的大小。 protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) onMeasure传入的两个参数是由上一层控件传入的大小,有多种情况,重写该方法时需要对计算控件的实际大小,然后调用setMeasuredDimension(int, int)设置实际大小。 onMeasure传入的widthMeasureSpec和heightMeasureSpec不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值。 我们需要通过int mode = MeasureSpec.getMode(widthMeasureSpec)得到模式,用int size = MeasureSpec.getSize(widthMeasureSpec)得到尺寸。 mode共有三种情况,取值分别为MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY, MeasureSpec.AT_MOST。 MeasureSpec.EXACTLY是精确尺寸,当我们将控件的layout_width或layout_height指定为具体数值时如andorid:layout_width="50dip", 或者为FILL_PARENT是,都是控件大小已经确定的情况,都是精确尺寸。 MeasureSpec.AT_MOST是最大尺寸,当控件的layout_width或layout_height指定为WRAP_CONTENT时, 控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。 MeasureSpec.UNSPECIFIED是未指定尺寸,这种情况不多,一般都是父控件是AdapterView,通过measure方法传入的模式。 因此,在重写onMeasure方法时要根据模式不同进行尺寸计算。下面代码就是一种比较典型的方式: */ if (specMode == MeasureSpec.EXACTLY) { // We were told how big to be result = specSize; } else { // Measure the text result = (int) mTextPaint.measureText(mText) + getPaddingLeft() + getPaddingRight(); if (specMode == MeasureSpec.AT_MOST) { // Respect AT_MOST value if that was what is called for by measureSpec result = Math.min(result, specSize); } } return result; }在这个方法中我们需要计算View的具体宽度了:
int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec);MeasureSpec提供了两个方法:getMode和getSize
这两个方法是获取计算模式和大小的,他们内部实现是用位操作的,我们看一下源码:
用一个int类型就可以将mode和size表示出来:int类型是32位的,这里用高2位表示mode.低30位表示大小。
我们可以在上面打印一下log看一下:
内部处理很简单,直接进行位相与操作就可以了:
一般来说,自定义控件都会去重写View的onMeasure方法,因为该方法指定该控件在屏幕上的大小。
protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec)
onMeasure传入的两个参数是由上一层控件传入的大小,有多种情况,重写该方法时需要对计算控件的实际大小,然后调用
setMeasuredDimension(int, int)设置实际大小。onMeasure传入的widthMeasureSpec和heightMeasureSpec不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值。我们需要通过
int mode = MeasureSpec.getMode(widthMeasureSpec)得到模式,
用int size = MeasureSpec.getSize(widthMeasureSpec)得到尺寸。
Mode共有三种情况,
取值分别为
MeasureSpec.UNSPECIFIED
MeasureSpec.EXACTLY
MeasureSpec.AT_MOST
A) MeasureSpec.EXACTLY是精确尺寸,当我们将控件的layout_width或layout_height指定为具体数值时如andorid:layout_width="50dip",或者为FILL_PARENT是,都是控件大小已经确定的情况,都是精确尺寸。
B) MeasureSpec.AT_MOST是最大尺寸,当控件的layout_width或layout_height指定为WRAP_CONTENT时,
控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。
C) MeasureSpec.UNSPECIFIED是未指定尺寸,这种情况不多,一般都是父控件是AdapterView,通过measure方法传入的模式。
因此,在重写onMeasure方法时要根据模式不同进行尺寸计算。下面代码就是一种比较典型的方式:
3)、重写onTouchEvent方法
获取坐标,计算坐标,然后通过invalidate和postInvalidate方法进行画面的刷新操作即可
关于这两个刷新方法的区别是:invalidate方法是在UI线程中调用的,postInvalidate可以在子线程中调用,而且最重要的是postInvalidate可以延迟调用
2、正式讲解一下例子
1)、第一个例子:自定义LabelView
这个View主要实现的功能和Android中提供的TextView差不多
代码:
package com.example.drawpathdemo; /* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Need the following import to get access to the app resources, since this // class is in a sub-package. import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; import android.util.Log; import android.view.View; /** * Example of how to write a custom subclass of View. LabelView * is used to draw simple text views. Note that it does not handle * styled text or right-to-left writing systems. * */ public class LabelView extends View { private Paint mTextPaint; private String mText; private int mAscent; /** * Constructor. This version is only needed if you will be instantiating * the object manually (not from a layout XML file). * @param context */ public LabelView(Context context) { super(context); initLabelView(); } /** * Construct object, initializing with any attributes we understand from a * layout file. These attributes are defined in * SDK/assets/res/any/classes.xml. * * @see android.view.View#View(android.content.Context, android.util.AttributeSet) */ public LabelView(Context context, AttributeSet attrs) { super(context, attrs); initLabelView(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView); CharSequence s = a.getString(R.styleable.LabelView_text); if (s != null) { setText(s.toString()); } // Retrieve the color(s) to be used for this view and apply them. // Note, if you only care about supporting a single color, that you // can instead call a.getColor() and pass that to setTextColor(). setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF000000)); int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0); if (textSize > 0) { setTextSize(textSize); } a.recycle(); } private final void initLabelView() { mTextPaint = new Paint(); mTextPaint.setAntiAlias(true); // Must manually scale the desired text size to match screen density mTextPaint.setTextSize(16 * getResources().getDisplayMetrics().density); mTextPaint.setColor(0xFF000000); setPadding(3, 3, 3, 3); } /** * Sets the text to display in this label * @param text The text to display. This will be drawn as one line. */ public void setText(String text) { mText = text; requestLayout(); invalidate(); } /** * Sets the text size for this label * @param size Font size */ public void setTextSize(int size) { // This text size has been pre-scaled by the getDimensionPixelOffset method mTextPaint.setTextSize(size); requestLayout(); invalidate(); } /** * Sets the text color for this label. * @param color ARGB value for the text */ public void setTextColor(int color) { mTextPaint.setColor(color); invalidate(); } /** * @see android.view.View#measure(int, int) */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); } /** * Determines the width of this view * @param measureSpec A measureSpec packed into an int * @return The width of the view, honoring constraints from measureSpec */ private int measureWidth(int measureSpec) { int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); Log.i("DEMO","measureSpec:"+Integer.toBinaryString(measureSpec)); Log.i("DEMO","specMode:"+Integer.toBinaryString(specMode)); Log.i("DEMO","specSize:"+Integer.toBinaryString(specSize)); /** * 一般来说,自定义控件都会去重写View的onMeasure方法,因为该方法指定该控件在屏幕上的大小。 protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) onMeasure传入的两个参数是由上一层控件传入的大小,有多种情况,重写该方法时需要对计算控件的实际大小,然后调用setMeasuredDimension(int, int)设置实际大小。 onMeasure传入的widthMeasureSpec和heightMeasureSpec不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值。 我们需要通过int mode = MeasureSpec.getMode(widthMeasureSpec)得到模式,用int size = MeasureSpec.getSize(widthMeasureSpec)得到尺寸。 mode共有三种情况,取值分别为MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY, MeasureSpec.AT_MOST。 MeasureSpec.EXACTLY是精确尺寸,当我们将控件的layout_width或layout_height指定为具体数值时如andorid:layout_width="50dip", 或者为FILL_PARENT是,都是控件大小已经确定的情况,都是精确尺寸。 MeasureSpec.AT_MOST是最大尺寸,当控件的layout_width或layout_height指定为WRAP_CONTENT时, 控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。 MeasureSpec.UNSPECIFIED是未指定尺寸,这种情况不多,一般都是父控件是AdapterView,通过measure方法传入的模式。 因此,在重写onMeasure方法时要根据模式不同进行尺寸计算。下面代码就是一种比较典型的方式: */ if (specMode == MeasureSpec.EXACTLY) { // We were told how big to be result = specSize; } else { // Measure the text result = (int) mTextPaint.measureText(mText) + getPaddingLeft() + getPaddingRight(); if (specMode == MeasureSpec.AT_MOST) { // Respect AT_MOST value if that was what is called for by measureSpec result = Math.min(result, specSize); } } return result; } /** * Determines the height of this view * @param measureSpec A measureSpec packed into an int * @return The height of the view, honoring constraints from measureSpec */ private int measureHeight(int measureSpec) { int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); mAscent = (int) mTextPaint.ascent(); if (specMode == MeasureSpec.EXACTLY) { // We were told how big to be result = specSize; } else { // Measure the text (beware: ascent is a negative number) result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop() + getPaddingBottom(); if (specMode == MeasureSpec.AT_MOST) { // Respect AT_MOST value if that was what is called for by measureSpec result = Math.min(result, specSize); } } return result; } /** * Render the text * * @see android.view.View#onDraw(android.graphics.Canvas) */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent, mTextPaint); } }首先来看一下构造方法方法:
/** * Construct object, initializing with any attributes we understand from a * layout file. These attributes are defined in * SDK/assets/res/any/classes.xml. * * @see android.view.View#View(android.content.Context, android.util.AttributeSet) */ public LabelView(Context context, AttributeSet attrs) { super(context, attrs); initLabelView(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView); CharSequence s = a.getString(R.styleable.LabelView_text); if (s != null) { setText(s.toString()); } // Retrieve the color(s) to be used for this view and apply them. // Note, if you only care about supporting a single color, that you // can instead call a.getColor() and pass that to setTextColor(). setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF000000)); int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0); if (textSize > 0) { setTextSize(textSize); } a.recycle(); }这里面我们用到了自定义属性的相关知识,不了解的同学可以转战:
http://blog.csdn.net/jiangwei0910410003/article/details/17006087
拿到字体的大小、颜色、内容。
然后再来看一下:
onDraw方法:
/** * Render the text * * @see android.view.View#onDraw(android.graphics.Canvas) */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent, mTextPaint); }这里使用drawText方法开始绘制文本内容,关于getPaddingLeft就是获取字体在View中的Padding值,但是这里有一个知识点:
mAscent变量,它是通过:
mAscent = (int) mTextPaint.ascent();获取到的。关于Paint的两个方法ascent和descent,这里解释一下:如下图
1.基准点是baseline
2.ascent:是baseline之上至字符最高处的距离
3.descent:是baseline之下至字符最低处的距离
4.leading:是上一行字符的descent到下一行的ascent之间的距离,也就是相邻行间的空白距离
5.top:是指的是最高字符到baseline的值,即ascent的最大值
6.bottom:是指最低字符到baseline的值,即descent的最大值
再来看一下设置字体颜色的方法:
/** * Sets the text color for this label. * @param color ARGB value for the text */ public void setTextColor(int color) { mTextPaint.setColor(color); invalidate(); }就是设置画笔的颜色,设置完之后需要生效,那么调用invalidate方法刷新一下即可。
上面的LabelView就定义好了。是不是很简单。没什么难度。主要就是定义一个画笔绘制文本,然后在计算view的大小即可。
下面来看一下用法:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:test="http://schemas.android.com/apk/res/com.example.drawpathdemo" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.drawpathdemo.MainActivity" > <com.example.drawpathdemo.LabelView android:id="@+id/progressview" android:layout_width="100dp" android:layout_height="wrap_content" test:text="aaa" test:textColor="#990099" test:textSize="16dp"/> </RelativeLayout>注意前缀test的定义方式:xmlns:test="http://schemas.android.com/apk/res/com.example.drawpathdemo"
xmlns:test="http://schemas.android.com/apk/res/包名"即可
效果图:
2)、第二个例子:自定义渐变的SeekBar
我们知道系统自带的SeekBar控件是没有渐变色的,比如下面这种效果:
看到这里我们可能会想到了,我们之前说到的Shader渲染对象了,这里我们选择LinearGradient渲染器来实现
代码:
package com.example.drawpathdemo; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.Shader; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; public class SpringProgressView extends View { private static final int[] SECTION_COLORS = {0xffffd300,Color.GREEN,0xff319ed4}; private float maxCount; private float currentCount; private Paint mPaint; private int mWidth,mHeight; private Bitmap bitMap; public SpringProgressView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(context); } public SpringProgressView(Context context, AttributeSet attrs) { super(context, attrs); initView(context); } public SpringProgressView(Context context) { super(context); initView(context); } private void initView(Context context) { /*bitMap = BitmapFactory.decodeResource(context.getResources(), R.drawable.scrubber_control_pressed_holo);*/ } @SuppressLint("DrawAllocation") @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPaint = new Paint(); mPaint.setAntiAlias(true); int round = mHeight/2; mPaint.setColor(Color.GRAY); RectF rectBg = new RectF(0, 0, mWidth, mHeight); canvas.drawRoundRect(rectBg, round, round, mPaint); mPaint.setColor(Color.WHITE); RectF rectBlackBg = new RectF(2, 2, mWidth-2, mHeight-2); canvas.drawRoundRect(rectBlackBg, round, round, mPaint); float section = currentCount/maxCount; RectF rectProgressBg = new RectF(3, 3, (mWidth-3)*section, mHeight-3); if(section <= 1.0f/3.0f){ if(section != 0.0f){ mPaint.setColor(SECTION_COLORS[0]); }else{ mPaint.setColor(Color.TRANSPARENT); } }else{ int count = (section <= 1.0f/3.0f*2 ) ? 2 : 3; int[] colors = new int[count]; System.arraycopy(SECTION_COLORS, 0, colors, 0, count); float[] positions = new float[count]; if(count == 2){ positions[0] = 0.0f; positions[1] = 1.0f-positions[0]; }else{ positions[0] = 0.0f; positions[1] = (maxCount/3)/currentCount; positions[2] = 1.0f-positions[0]*2; } positions[positions.length-1] = 1.0f; LinearGradient shader = new LinearGradient(3, 3, (mWidth-3)*section, mHeight-3, colors,null, Shader.TileMode.MIRROR); mPaint.setShader(shader); } canvas.drawRoundRect(rectProgressBg, round, round, mPaint); //canvas.drawBitmap(bitMap, rectProgressBg.right-20, rectProgressBg.top-4, null); } private int dipToPx(int dip) { float scale = getContext().getResources().getDisplayMetrics().density; return (int) (dip * scale + 0.5f * (dip >= 0 ? 1 : -1)); } public void setMaxCount(float maxCount) { this.maxCount = maxCount; } public void setCurrentCount(float currentCount) { this.currentCount = currentCount > maxCount ? maxCount : currentCount; invalidate(); } public float getMaxCount() { return maxCount; } public float getCurrentCount() { return currentCount; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (widthSpecMode == MeasureSpec.EXACTLY || widthSpecMode == MeasureSpec.AT_MOST) { mWidth = widthSpecSize; } else { mWidth = 0; } if (heightSpecMode == MeasureSpec.AT_MOST || heightSpecMode == MeasureSpec.UNSPECIFIED) { mHeight = dipToPx(15); } else { mHeight = heightSpecSize; } setMeasuredDimension(mWidth, mHeight); } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); Log.i("DEMO", "x:"+x + ",y:"+y); getParent().requestDisallowInterceptTouchEvent(true); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: moved(x, y); break; case MotionEvent.ACTION_MOVE: moved(x, y); break; case MotionEvent.ACTION_UP: moved(x, y); break; } return true; } private void moved(float x,float y){ if(x > mWidth){ return; } currentCount = maxCount * (x/mWidth); invalidate(); } }主要看一下onDraw方法:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPaint = new Paint(); mPaint.setAntiAlias(true); int round = mHeight/2; mPaint.setColor(Color.GRAY); RectF rectBg = new RectF(0, 0, mWidth, mHeight); canvas.drawRoundRect(rectBg, round, round, mPaint); mPaint.setColor(Color.WHITE); RectF rectBlackBg = new RectF(2, 2, mWidth-2, mHeight-2); canvas.drawRoundRect(rectBlackBg, round, round, mPaint); float section = currentCount/maxCount; RectF rectProgressBg = new RectF(3, 3, (mWidth-3)*section, mHeight-3); if(section <= 1.0f/3.0f){ if(section != 0.0f){ mPaint.setColor(SECTION_COLORS[0]); }else{ mPaint.setColor(Color.TRANSPARENT); } }else{ int count = (section <= 1.0f/3.0f*2 ) ? 2 : 3; int[] colors = new int[count]; System.arraycopy(SECTION_COLORS, 0, colors, 0, count); float[] positions = new float[count]; if(count == 2){ positions[0] = 0.0f; positions[1] = 1.0f-positions[0]; }else{ positions[0] = 0.0f; positions[1] = (maxCount/3)/currentCount; positions[2] = 1.0f-positions[0]*2; } positions[positions.length-1] = 1.0f; LinearGradient shader = new LinearGradient(3, 3, (mWidth-3)*section, mHeight-3, colors,null, Shader.TileMode.MIRROR); mPaint.setShader(shader); } canvas.drawRoundRect(rectProgressBg, round, round, mPaint); //canvas.drawBitmap(bitMap, rectProgressBg.right-20, rectProgressBg.top-4, null); }这里首先绘制一个圆角矩形,绘制了两个作为SeekBar的背景图片:
RectF rectBg = new RectF(0, 0, mWidth, mHeight); canvas.drawRoundRect(rectBg, round, round, mPaint); mPaint.setColor(Color.WHITE); RectF rectBlackBg = new RectF(2, 2, mWidth-2, mHeight-2); canvas.drawRoundRect(rectBlackBg, round, round, mPaint);
SeekBar最大的值:maxCount
SeekBar当前的值:currentCount
float section = currentCount/maxCount; RectF rectProgressBg = new RectF(3, 3, (mWidth-3)*section, mHeight-3); if(section <= 1.0f/3.0f){ if(section != 0.0f){ mPaint.setColor(SECTION_COLORS[0]); }else{ mPaint.setColor(Color.TRANSPARENT); } }else{ int count = (section <= 1.0f/3.0f*2 ) ? 2 : 3; int[] colors = new int[count]; System.arraycopy(SECTION_COLORS, 0, colors, 0, count); float[] positions = new float[count]; if(count == 2){ positions[0] = 0.0f; positions[1] = 1.0f-positions[0]; }else{ positions[0] = 0.0f; positions[1] = (maxCount/3)/currentCount; positions[2] = 1.0f-positions[0]*2; } positions[positions.length-1] = 1.0f; LinearGradient shader = new LinearGradient(3, 3, (mWidth-3)*section, mHeight-3, colors,null, Shader.TileMode.MIRROR); mPaint.setShader(shader); } canvas.drawRoundRect(rectProgressBg, round, round, mPaint);计算好了渐变色的距离,我们在用drawRoundRect方法绘制一个渐变色的圆角矩形即可。
再看一下onTouchEvent方法:
@Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); Log.i("DEMO", "x:"+x + ",y:"+y); getParent().requestDisallowInterceptTouchEvent(true); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: moved(x, y); break; case MotionEvent.ACTION_MOVE: moved(x, y); break; case MotionEvent.ACTION_UP: moved(x, y); break; } return true; } private void moved(float x,float y){ if(x > mWidth){ return; } currentCount = maxCount * (x/mWidth); invalidate(); }这里就会计算手指的坐标,然后通过手指移动的x坐标和SeekBar的宽度的比例在计算出当前的currentCount值,然后刷新一下界面即可。
在使用的时候我们需要设置最大值和当前值:
SpringProgressView view = (SpringProgressView)findViewById(R.id.view); view.setMaxCount(100); view.setCurrentCount(30);
效果图如下:
3)、第三个例子:自定义闪烁文本的TextView
效果如下:
不多说,直接上代码:
package com.example.drawpathdemo; import android.content.Context; import android.graphics.Canvas; import android.graphics.LinearGradient; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Shader; import android.util.AttributeSet; import android.widget.TextView; public class MyTextView extends TextView { private LinearGradient mLinearGradient; private Matrix mGradientMatrix; private Paint mPaint; private int mViewWidth = 0; private int mTranslate = 0; private boolean mAnimating = true; public MyTextView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (mViewWidth == 0) { mViewWidth = getMeasuredWidth(); if (mViewWidth > 0) { mPaint = getPaint(); mLinearGradient = new LinearGradient(0,0,mViewWidth,0, new int[] { 0x33ffffff, 0xffffffff, 0x33ffffff },null,Shader.TileMode.CLAMP); mPaint.setShader(mLinearGradient); mGradientMatrix = new Matrix(); } } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mAnimating && mGradientMatrix != null) { mTranslate += mViewWidth / 10; if (mTranslate > 2 * mViewWidth) { mTranslate = -mViewWidth; } mGradientMatrix.setTranslate(mTranslate, 0); mLinearGradient.setLocalMatrix(mGradientMatrix); postInvalidateDelayed(50); } } }
这里我们详细来看一下onSizeChanged方法:
这个方法在TextView大小发生改变的时候会调用,一般我们在xml中已经把文本设置好了,只会调用一次。我们在这里面进行一次啊初始化操作。我们首先用getPaint方法拿到TextView的画笔,然后定义一个LinearGradient渲染器,但是我们现在还需要一个功能就是那个白色渐变能够自动从左往右移,那么就会使用到平移动画。这里就会使用到一个新的对象:Matrix
矩阵对象,我们如果了解图形学的话,会知道矩阵是重要的一个概念,他可以实现图片的转化,各种动画的实现等。
Android中我们可以给一个渲染器设置一个变化矩阵。对于矩阵我们可以设置平移,旋转,缩放的动画。那么我们这里就是用他的平移动画:
我们就把LinearGradient这个比作一个长方形,如上图是初始化的位置在手机屏幕的最左边,要运动到屏幕的最右边就需要2*width的长度。
我们在onDraw方法中就开始计算移动的坐标,然后调用postInvalidate方法延迟去刷新界面。
下面来看一下效果图:
这个例子中我们学习到了渲染器可以有动画的,我们可以对渲染器进行动画操作,这个知识点,我们在后面还会在用到。
4)、第四个例子:颜色选择器
我们知道Office办公软件中有一个功能就是有一个颜色选择器板:
那么在Android中我们,我们就来实现以下这样的效果。其实我们主要介绍的是选择器版面的View,因为这里会用到Shader,上面这张图片我们知道可以使用LinearGradient渲染器实现,但是我们这里定义一个圆形选择器,效果图如下:
那么这里我们看到效果图之后,知道应该使用扫描渲染器:SweepGradient
原理:我们在onTouchEvent方法中获取用户点击了那一块颜色区域然后计算出它在哪一块颜色区域即可。
代码:
package com.example.drawpathdemo; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorMatrix; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.SweepGradient; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; public class ColorPickerView extends View { private Paint mPaint;//渐变色环画笔 private Paint mCenterPaint;//中间圆画笔 private int[] mColors;//渐变色环颜色 private OnColorChangedListener mListener;//颜色改变回调 private static final int CENTER_X = 200; private static final int CENTER_Y = 200; private static final int CENTER_RADIUS = 32; public ColorPickerView(Context context){ super(context); } public ColorPickerView(Context context, AttributeSet attrs) { super(context, attrs); mColors = new int[] {//渐变色数组 0xFFFF0000, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFF00FF00, 0xFFFFFF00, 0xFFFF0000 }; Shader s = new SweepGradient(0, 0, mColors, null); //初始化渐变色画笔 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setShader(s); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(32); //初始化中心园画笔 mCenterPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mCenterPaint.setColor(Color.RED); mCenterPaint.setStrokeWidth(15); } public ColorPickerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private boolean mTrackingCenter; private boolean mHighlightCenter; @Override protected void onDraw(Canvas canvas) { float r = CENTER_X - mPaint.getStrokeWidth()*0.5f; //移动中心 canvas.translate(CENTER_X, CENTER_Y); //画出色环和中心园 canvas.drawOval(new RectF(-r, -r, r, r), mPaint); canvas.drawCircle(0, 0, CENTER_RADIUS, mCenterPaint); if (mTrackingCenter) { int c = mCenterPaint.getColor(); mCenterPaint.setStyle(Paint.Style.STROKE); if (mHighlightCenter) { mCenterPaint.setAlpha(0xFF); } else { mCenterPaint.setAlpha(0x80); } canvas.drawCircle(0, 0, CENTER_RADIUS + mCenterPaint.getStrokeWidth(), mCenterPaint); mCenterPaint.setStyle(Paint.Style.FILL); mCenterPaint.setColor(c); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(CENTER_X*2, CENTER_Y*2); } private int ave(int s, int d, float p) { return s + java.lang.Math.round(p * (d - s)); } private int interpColor(int colors[], float unit) { if (unit <= 0) { return colors[0]; } if (unit >= 1) { return colors[colors.length - 1]; } /** * 1,2,3,4,5 * 0.6*4=2.4 * i=2,p=0.4 */ float p = unit * (colors.length - 1); int i = (int)p; p -= i; // now p is just the fractional part [0...1) and i is the index int c0 = colors[i]; int c1 = colors[i+1]; int a = ave(Color.alpha(c0), Color.alpha(c1), p); int r = ave(Color.red(c0), Color.red(c1), p); int g = ave(Color.green(c0), Color.green(c1), p); int b = ave(Color.blue(c0), Color.blue(c1), p); return Color.argb(a, r, g, b); } private static final float PI = 3.1415926f; @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX() - CENTER_X; float y = event.getY() - CENTER_Y; boolean inCenter = java.lang.Math.sqrt(x*x + y*y) <= CENTER_RADIUS; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mTrackingCenter = inCenter; if (inCenter) { mHighlightCenter = true; invalidate(); break; } case MotionEvent.ACTION_MOVE: if (mTrackingCenter) { if (mHighlightCenter != inCenter) { mHighlightCenter = inCenter; invalidate(); } } else { float angle = (float)java.lang.Math.atan2(y, x); // need to turn angle [-PI ... PI] into unit [0....1] float unit = angle/(2*PI); if (unit < 0) { unit += 1; } mCenterPaint.setColor(interpColor(mColors, unit)); invalidate(); } break; case MotionEvent.ACTION_UP: if (mTrackingCenter) { if (inCenter) { if(mListener != null) mListener.colorChanged(mCenterPaint.getColor()); } mTrackingCenter = false; invalidate(); } break; } return true; } public interface OnColorChangedListener{ public void colorChanged(int color); } }
public ColorPickerView(Context context, AttributeSet attrs) { super(context, attrs); mColors = new int[] {//渐变色数组 0xFFFF0000, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFF00FF00, 0xFFFFFF00, 0xFFFF0000 }; Shader s = new SweepGradient(0, 0, mColors, null); //初始化渐变色画笔 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setShader(s); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(32); //初始化中心园画笔 mCenterPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mCenterPaint.setColor(Color.RED); mCenterPaint.setStrokeWidth(15); }定义一个渲染器,初始化两个画笔,一个是画外边的选择器圆环,我们这里是通过设置轮廓的宽度来实现的,这样做也是比较简单的。
再来看一下onDraw方法:
@Override protected void onDraw(Canvas canvas) { float r = CENTER_X - mPaint.getStrokeWidth()*0.5f; //移动中心 canvas.translate(CENTER_X, CENTER_Y); //画出色环和中心园 canvas.drawOval(new RectF(-r, -r, r, r), mPaint); canvas.drawCircle(0, 0, CENTER_RADIUS, mCenterPaint); if (mTrackingCenter) { int c = mCenterPaint.getColor(); mCenterPaint.setStyle(Paint.Style.STROKE); if (mHighlightCenter) { mCenterPaint.setAlpha(0xFF); } else { mCenterPaint.setAlpha(0x80); } canvas.drawCircle(0, 0, CENTER_RADIUS + mCenterPaint.getStrokeWidth(), mCenterPaint); mCenterPaint.setStyle(Paint.Style.FILL); mCenterPaint.setColor(c); } }这里开始绘制图形,调用了canvas.translate方法,将画布移到圆环的中心,这样做之后,我们下面的在绘制圆形的时候,就不需要那么复杂的计算圆心的坐标了。
这里有一个判断,我们在下面会说道,mTrackingCenter表示点击了中间的圆形区域,mHightlightCenter表示从点击了圆形之后,移动,这里只是做了透明度的处理,说的有点抽象,可以自己试验一下就知道了。
在来看一下onTouchEvent方法:
@Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX() - CENTER_X; float y = event.getY() - CENTER_Y; boolean inCenter = java.lang.Math.sqrt(x*x + y*y) <= CENTER_RADIUS; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mTrackingCenter = inCenter; if (inCenter) { mHighlightCenter = true; invalidate(); break; } case MotionEvent.ACTION_MOVE: if (mTrackingCenter) { if (mHighlightCenter != inCenter) { mHighlightCenter = inCenter; invalidate(); } } else { float angle = (float)java.lang.Math.atan2(y, x); // need to turn angle [-PI ... PI] into unit [0....1] float unit = angle/(2*PI); if (unit < 0) { unit += 1; } mCenterPaint.setColor(interpColor(mColors, unit)); invalidate(); } break; case MotionEvent.ACTION_UP: if (mTrackingCenter) { if (inCenter) { if(mListener != null) mListener.colorChanged(mCenterPaint.getColor()); } mTrackingCenter = false; invalidate(); } break; } return true; }这里开始的时候就先判断触发点所在的区域:
boolean inCenter = java.lang.Math.sqrt(x*x + y*y) <= CENTER_RADIUS;这个公式很简单:就是判断当前触摸点的坐标是否在半径为CENTER_RADIUS的圆中
然后当移动的时候,我们需要计算触发点所在的颜色区域:
float angle = (float)java.lang.Math.atan2(y, x); // need to turn angle [-PI ... PI] into unit [0....1] float unit = angle/(2*PI); if (unit < 0) { unit += 1; }这里首先算出触发点的到圆形的角度,然后interpColor(mColors, unit)方法算出颜色:
private int interpColor(int colors[], float unit) { if (unit <= 0) { return colors[0]; } if (unit >= 1) { return colors[colors.length - 1]; } /** * 1,2,3,4,5 * 0.6*4=2.4 * i=2,p=0.4 */ float p = unit * (colors.length - 1); int i = (int)p; p -= i; // now p is just the fractional part [0...1) and i is the index int c0 = colors[i]; int c1 = colors[i+1]; int a = ave(Color.alpha(c0), Color.alpha(c1), p); int r = ave(Color.red(c0), Color.red(c1), p); int g = ave(Color.green(c0), Color.green(c1), p); int b = ave(Color.blue(c0), Color.blue(c1), p); return Color.argb(a, r, g, b); }第一个参数是颜色数组,第二个是区域单元。
这个方法很简单就是用一个区域值unit到颜色数组中找到其对应的色值区域
打个比方:在高中的时候我们学习函数的时候,总是提到定义域和值域
这里定义域就是unit,值域就是色值区域,这样理解就简单了
不过这里用到了一个方法:Color.red/green/blue,获取一个色值的三基色的值,然后在计算比例:
private int ave(int s, int d, float p) { return s + java.lang.Math.round(p * (d - s)); }这个方法也很简单,算出两个值之间的指定比例的值公式为:
value = http://www.mamicode.com/p1 + p(p2-p1)
其中p1是区域的开始值,p是比例,p2是区域的结束值
p的值为[0,1],当p=0时,value=http://www.mamicode.com/p1就是开始值,当p=1时,value=p2就是结束值
这样原理就解释清楚了吧,如果还是不清楚的同学,那我也没办法了。
效果:
5)、第五个例子:自定义圆形渐变的SeekBar
这个例子和上面的长条渐变的SeekBar差不多的,只是这次是圆形的,效果图如下:
那么我们就知道这次应该使用扫描梯度的渲染器:SweepGradient
这里面我们绘制圆环的时候,使用的是绘制两个同心圆的方式的。
来看一下代码:
onDraw方法:
@Override protected void onDraw(Canvas canvas) { float section = progress/maxProgress; /*dx = getXFromAngle(); dy = getYFromAngle();*/ /*LinearGradient shader = new LinearGradient(0,0,dx,dy, colors,null, Shader.TileMode.CLAMP); circleColor.setShader(shader);*/ SweepGradient shader = new SweepGradient(cx, cy, SECTION_COLORS, null); Matrix matrix = new Matrix(); matrix.setRotate(-90,cx,cy); shader.setLocalMatrix(matrix); circleColor.setShader(shader); canvas.drawCircle(cx, cy, outerRadius, circleRing); canvas.drawArc(rect, startAngle, angle, true, circleColor); canvas.drawCircle(cx, cy, innerRadius-40, innerColor); /*if(SHOW_SEEKBAR){ drawMarkerAtProgress(canvas); }*/ super.onDraw(canvas); }这里,我们一样的有两个变量:progress表示当前进度条的值,maxProgress表示进度条最大的值。然后定义一个梯度渲染器。然后开始绘制圆环,首先绘制外围的圆形,然后继续绘制一个扇形(用渲染器),最后在绘制内部的圆形。
这里在操作的过程中需要注意的是,将SweepGradient渲染器逆时针旋转90度,不然结果是不正确的。
在来看一下onTouchEvent方法:
@Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); boolean up = false; this.getParent().requestDisallowInterceptTouchEvent(true); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: moved(x, y, up); break; case MotionEvent.ACTION_MOVE: moved(x, y, up); break; case MotionEvent.ACTION_UP: up = true; moved(x, y, up); break; } return true; } /** * Moved. * * @param x * the x * @param y * the y * @param up * the up */ private void moved(float x, float y, boolean up) { float distance = (float) Math.sqrt(Math.pow((x - cx), 2) + Math.pow((y - cy), 2)); if (distance < outerRadius + adjustmentFactor && distance > innerRadius - adjustmentFactor && !up) { IS_PRESSED = true; markPointX = (float) (cx + outerRadius * Math.cos(Math.atan2(x - cx, cy - y) - (Math.PI /2))); markPointY = (float) (cy + outerRadius * Math.sin(Math.atan2(x - cx, cy - y) - (Math.PI /2))); float degrees = (float) ((float) ((Math.toDegrees(Math.atan2(x - cx, cy - y)) + 360.0)) % 360.0); // and to make it count 0-360 if (degrees < 0) { degrees += 2 * Math.PI; } setAngle(Math.round(degrees)); invalidate(); } else { IS_PRESSED = false; invalidate(); } }这里主要是计算扇形的角度。
还需要在代码中设置一下当前进度和最大进度值:
CircularSeekBar view = (CircularSeekBar)findViewById(R.id.progressview); view.setMaxProgress(100); view.setProgress(20);
运行结果:
6)、第六个例子:模仿360手机卫士中流量监控的曲线图
这里面主要用到了Path对象,因为我们绘制折线图,用Path是最好不过了,然后在用LinearGradient渲染器,这里需要注意的是,Path绘制的折线图一定要是封闭的,不然这个渲染是没有任何效果的,原理图如下:
我们绘制一个封闭的Path折线图(相当于多边形)
代码如下:
onDraw方法
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawHLines(canvas); drawVLines(canvas); drawCurveLines(canvas); }
drawHLines方法:
private void drawHLines(Canvas canvas) { for(int i = 0;i<=COLUM_VERTIAL_NUM;i++){ canvas.drawLine(0, mColumWidth *i, mColumWidth * (mDays), mColumWidth *i, mPaint); } }
private void drawVLines(Canvas canvas) { for (int i = 0; i < mDays; i++) { canvas.drawLine(mColumWidth * (i + 0.5f), 0, mColumWidth * (i + 0.5f), mColumWidth * COLUM_VERTIAL_NUM, mPaint); } }
private void drawCurveLines(Canvas canvas) { Path path = new Path(); for (int i = 0; i < mDays; i++) { List<Float> floats = CommonUtil.getRatioList(allDayTotalValueList); int flow; float ratio; try{ flow = allDayTotalValueList.get(i); ratio = floats.get(i); }catch (Exception e){ flow = 0; ratio = 0; } if (i == 0) { path.moveTo(mColumWidth * (i + 0.5f), getYByFlow((int)(ratio * mColumWidth * COLUM_VERTIAL_NUM))); } path.lineTo(mColumWidth * (i + 0.5f), getYByFlow((int)(ratio * mColumWidth * COLUM_VERTIAL_NUM))); canvas.drawCircle(mColumWidth * (i + 0.5f), getYByFlow((int)(ratio * mColumWidth * COLUM_VERTIAL_NUM)), RADIUS, mCirclePaint); if(CommonUtil.DAY_OF_MONTH() == (i+1)){ canvas.drawCircle(mColumWidth * (i + 0.5f), getYByFlow(0), RADIUS, mTodayCirclePaint); } drawFlowText(canvas,flow,ratio,i); drawDateText(canvas,i); } canvas.drawPath(path, mCurvePaint); drawGraintColor(canvas, path); }
其他绘制方法:
private void drawFlowText(Canvas canvas,int value,float ratio,int position){ String text = CommonUtil.formatSize(value); float textHeight = mDatePaint.descent() - mDatePaint.ascent(); float textOffset = (textHeight / 2) - mDatePaint.descent(); float width = mDatePaint.measureText(text); canvas.drawText(text, mColumWidth * (position + 0.5f) - width / 2, getYByFlow((int)(ratio * mColumWidth * COLUM_VERTIAL_NUM)) - textHeight/2 + textOffset, mDatePaint); } private void drawDateText(Canvas canvas,int position){ String text = ""; if(CommonUtil.DAY_OF_MONTH() == (position+1)){ text = "今日"; }else{ text = mCurrentMonth+"/" +(position+1); } float textHeight = mDatePaint.descent() - mDatePaint.ascent(); float textOffset = (textHeight / 2) - mDatePaint.descent(); float width = mDatePaint.measureText(text); canvas.drawText(text, mColumWidth * (position + 0.5f) - width / 2, mColumWidth * COLUM_VERTIAL_NUM + textHeight/2+ textOffset, mDatePaint); } private void drawGraintColor(Canvas canvas,Path path){ path.lineTo(mColumWidth * (mDays - 0.5f), mColumWidth * COLUM_VERTIAL_NUM); path.lineTo(mColumWidth * 0.5f, mColumWidth * COLUM_VERTIAL_NUM); canvas.drawPath(path,mGraintPaint); }
layout.xml中:
<HorizontalScrollView android:layout_width="match_parent" android:paddingTop="5dp" android:paddingBottom="5dp" android:layout_height="wrap_content"> <com.example.drawpathdemo.CurveView android:id="@+id/curve_view" android:layout_width="match_parent" android:layout_height="match_parent" /> </HorizontalScrollView>
这里使用水平的滚动视图
CurveView curve = (CurveView)findViewById(R.id.curve_view); List<Integer> list = new ArrayList<Integer>(31); for(int i=0;i<31;i++){ list.add(new Random().nextInt(1000000)); } curve.setAllDayTotalValue(list);这里初始化一个31天的每天消耗的流量大小值,采用随机数的。在CommonUtil工具类中有数值的格式转化。
在这31大小的list中找到最大值,然后和CurveView中折线图的大小进行比例计算,不然折线图的最高点会超出CurveView。
运行结果:
效果没有360的好,但是差不多了,在详细的调一下就可以了。哈哈~~
到这里我们就介绍完了Android中自定义视图的基础知识了。这篇文章很长,本来想分开讲解的,最后发现很多知识都是串联在一起的,所以就在一篇文章中进行讲解了,其实没什么难度的。
总结
在Android中我们绘图的时候其实有两种方式,一种是我们这篇文章主要讲的继承View之后,拿到画布对象Canvas然后进行绘制,这种方式主要用在自定义视图中,而且这种方式是在UI线程中进行的,那么除了这种方式,我们还可以使用SurfaceView进行绘图,这种方式效率高点,而且可以在子线程中进行的,一般用于游戏开发中,示例代码:
//内部类的内部类 class MyThread implements Runnable{ @Override public void run() { Canvas canvas = holder.lockCanvas(null);//获取画布 Paint mPaint = new Paint(); mPaint.setColor(Color.BLUE); canvas.drawRect(new RectF(40,60,80,80), mPaint); holder.unlockCanvasAndPost(canvas);//解锁画布,提交画好的图像 } }这里首先需要获取画布,其实就是给画布加一个锁,当画完了之后,就释放锁。所以可以在多线程中进行绘制。
这一篇文章是绘图+自定义视图的基础,后续还会在开发过程中遇到一些高级的自定义视图,那时候我还会一一进行讲解的。
Android中自定义视图View之---开发案例