android自定义View学习笔记(一)

概论

  •            自己也学Android不久,就几个月的时间,自己做编程也有两年多的时间,都是在一些小公司,而还比较清闲基本一一周的实际工作两三天就搞定了,所以进步不是很大。看着同学的工资蹭蹭的往上涨,而自己拿的工资也是那么一点点。看别人博客说写博客是一个很好进步技术的方式,于是我也来尝试。之前都是转载别人的博客内容,自己实际上写得很少。不扯淡了,本篇文章是学习了鸿洋大神博客学到,自己也来学习写写。

自定义view步骤

    1. 分析出自定义view所需要的属性

    2. 继承view在钩针器中获取自己定义的属性

    3. 重写view中onMeasure()方法

    4. 重写view中的onDraw()方法       

       基本上是这四个步骤, 其中onMeasure()方法主要是用于计算view空间的宽高的,所以不一定必须重写(如果不重写默认是占满全屏)。onDraw()方法主要是绘制view。

       具体讲解一下步骤,现在Android工程的/res/values新建一个名为attrs.xml的文件,此文件就是用来定义自定义属性的,Android是用attr表现自定义属性。

        android自定义View学习笔记(一)

    

<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
自定义样式 
format是值该属性的取值类型一共有:string,color,demension,integer,enum,reference,float,boolean,fraction,flag;
"reference" //引用  
"color" //颜色  
"boolean" //布尔值  
"dimension" //尺寸值  
"float" //浮点值  
"integer" //整型值  
"string" //字符串  
"fraction" //百分数,比如200%  
-->
    <attr name="text" format="string" />
    <attr name="text_size" format="dimension" />
    <attr name="text_origin_color" format="color|reference" />
    <attr name="text_change_color" format="color|reference" />
    <attr name="progress" format="float" />
    <attr name="direction">
        <enum name="left" value="0" />
        <enum name="right" value="1" />
        <enum name="up" value="2" />
        <enum name="down" value="3" />
    </attr>
    <declare-styleable name="qiu_ColorTrack">
        <attr name="text" />
        <attr name="text_size" />
        <attr name="text_change_color" />
        <attr name="text_origin_color" />
        <attr name="progress" />
        <attr name="direction" />
    </declare-styleable>
</resources>

    上面代码注释中已经讲解每个format属性值的意思了。

     接下来我就是在自定义view的构造器中要获取我们自定义属性,请看代码

public class MyColorTrackView extends View {

	private String mText; // 显示的字
	private int mTextSize = dp2px(16);
	private int mOriginColor = 0xff000000; // 开始的颜色
	private int mChangeColor = 0xffff0000;// 变化的颜色
	private float mProgress;// 进度
	private int mDirection;// 方向

	public final static int DIRECTION_LEFT = 0;
	public final static int DIRECTION_RIGHT = 1;
	public final static int DIRECTION_UP = 2;
	public final static int DIRECTION_DOWN = 3;

	public void setDirection(int mDirection) {
		this.mDirection = mDirection;
	}

	private int mRealWidth; // 字符串的真实宽度
	private int mRealHeight; // 字符串的真实高度

	private Rect mTextBounds;

	private Paint mPaint;

	private int mTextStartX;// 开始的位置x坐标
	private int mTextStartY;// 开始位置的y坐标
	private int mTextWidth;
	private int mTextHeight;

	public MyColorTrackView(Context context) {
		this(context, null);
	}

	public MyColorTrackView(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public MyColorTrackView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
				R.styleable.qiu_ColorTrack, defStyle, 0);
		for (int i = 0; i < a.getIndexCount(); i++) {
			int attr = a.getIndex(i);
			switch (attr) {
			case R.styleable.qiu_ColorTrack_text_origin_color:
				mOriginColor = a.getColor(attr, Color.BLACK);
				break;
			case R.styleable.qiu_ColorTrack_text_change_color:
				mChangeColor = a.getColor(attr, Color.RED);
				break;
			case R.styleable.qiu_ColorTrack_progress:
				mProgress = a.getFloat(attr, 0);
				break;
			case R.styleable.qiu_ColorTrack_text:
				mText = a.getString(attr);
				break;
			case R.styleable.qiu_ColorTrack_text_size:
				mTextSize = a.getDimensionPixelSize(attr, mTextSize);
				break;
			case R.styleable.qiu_ColorTrack_direction:
				mTextSize = a.getInt(attr, DIRECTION_LEFT);
				break;
			}
		}
		a.recycle();
		//获取抗锯齿的paint,还可以通过mPaint.setAntiAlias(true);来设置抗锯齿	
		mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);	
		mPaint.setTextSize(mTextSize);

		mTextWidth = (int) mPaint.measureText(mText);// 获取字符的实际长度
		FontMetrics fm = mPaint.getFontMetrics();
		mTextHeight = (int) Math.ceil(fm.descent - fm.ascent);// 获取字的实际高度
		mTextBounds = new Rect();
		mPaint.getTextBounds(mText, 0, mText.length(), mTextBounds);
	}

    继承view之后的构造器,我们要在参数最长的那个构造器中获取属性,其他参数的构造器用this()调用即可。

    构造器中获取自定义属性基本都是模板代码。获取属性之后可以初始化一些对象如:Paint Rect等。其中获取长度的时候我们获取到的可能是sp dip等的单位,我们要把他转换成px来显示,转换的方法看下面代码

//将sp的值转换成px
private int sp2px(int val) {
	return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, val,
				getResources().getDisplayMetrics());
}
//将dip单位的值转换成px
private int dp2px(int val) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
	            val, getResources().getDisplayMetrics());
}

    重写onMeasure

     系统帮我们测量的高度和宽度都是MATCH_PARNET,当我们设置明确的宽度和高度时,系统帮我们测量的结果就    我们设置的结果,

*当我们设置为WRAP_CONTENT,或者MATCH_PARENT系统帮我们测量的结果就是MATCH_PARENT的长度。

* 所以,当设置了WRAP_CONTENT时,我们需要自己进行测量,即重写onMesure方法”: 

* 重写之前先了解MeasureSpec的specMode,一共三种类型:

* EXACTLY:一般是设置了明确的值或者是MATCH_PARENT

* AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT

* UNSPECIFIED:表示子布局想要多大就多大,很少使用

    还要分析我们控件里面有哪些成员怎么摆放,是文字还是图片,都要获取其宽高进行计算整个控件的大小。

@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		int width = measureWidth(widthMeasureSpec);
		int height = measureHeigth(heightMeasureSpec);
		setMeasuredDimension(width, height);

		mRealWidth = width - getPaddingLeft() - getPaddingRight();
		mRealHeight = height - getPaddingTop() - getPaddingBottom();
		mTextStartX = mRealWidth / 2 - mTextWidth / 2;
		mTextStartY = mRealHeight / 2 + mTextHeight / 2 + getPaddingTop() / 2;
	}

	private int measureHeigth(int measureSpec) {
		int size = MeasureSpec.getSize(measureSpec);
		int mode = MeasureSpec.getMode(measureSpec);
		int result = 0;
		switch (mode) {
		case MeasureSpec.UNSPECIFIED:// 无限大
		case MeasureSpec.AT_MOST:
			result = Math.min(size, mTextHeight + getPaddingTop()
					+ getPaddingBottom());
			break;
		case MeasureSpec.EXACTLY:
			result = size + getPaddingTop() + getPaddingBottom();
			break;
		}
		return result;
	}

	/**
	 * 计算宽
	 * 
	 * @param widthMeasureSpec
	 * @return
	 */
	private int measureWidth(int measureSpec) {
		int size = MeasureSpec.getSize(measureSpec);
		int mode = MeasureSpec.getMode(measureSpec);
		int result = 0;
		switch (mode) {
		case MeasureSpec.UNSPECIFIED:// 无限大
		case MeasureSpec.AT_MOST:
			result = Math.min(size, mTextWidth + getPaddingLeft()
					+ getPaddingRight());
			break;
		case MeasureSpec.EXACTLY:
			result = size + getPaddingLeft() + getPaddingRight();
			break;
		}
		return result;
	}

    通过来获取模式和大小,getSize都是系统给我们计算的大小一般是MATCH_PARNET或者具体的值

int size = MeasureSpec.getSize(measureSpec);
int mode = MeasureSpec.getMode(measureSpec);

    上面代码是获取文字的大小,注意的是计算控件真实大小的时候要加上getPadding,要不然文字会部分显示不出来。

    最后就是重写onDraw

@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);

		if (mDirection == DIRECTION_LEFT) {
			drawOriginLeft(canvas);
			drawChangeLeft(canvas);
		} else if (mDirection == DIRECTION_RIGHT) {
			drawChangeRight(canvas);
			drawOriginRight(canvas);
		} else if (mDirection == DIRECTION_UP) {
			drawOriginUp(canvas);
			drawChangeUp(canvas);
		} else {
			drawOriginDown(canvas);
			drawChangeDown(canvas);
		}
	}

	private void drawChangeDown(Canvas canvas) {
		drawText(canvas, mChangeColor, mTextStartY, (int) (mTextStartY - mTextHeight
				*mProgress));
	}

	private void drawOriginDown(Canvas canvas) {
		drawText(canvas, mOriginColor, mTextStartY - mTextHeight,
				(int) (mTextStartY - mTextHeight * mProgress));
	}

	private void drawOriginUp(Canvas canvas) {

		drawText(canvas, mOriginColor, (int) (mTextStartY - mTextHeight
				* (1 - mProgress)), mTextStartY);
	}

	private void drawChangeUp(Canvas canvas) {
		drawText(canvas, mChangeColor, mTextStartY - mTextHeight,
				(int) (mTextStartY - mTextHeight * (1 - mProgress)));
	}

	private void drawChangeRight(Canvas canvas) {
		drawText(canvas, mChangeColor, (int) (mTextStartX + (1 - mProgress)
				* mTextWidth), mTextStartX + mTextWidth);
	}

	private void drawOriginRight(Canvas canvas) {
		drawText(canvas, mOriginColor, mTextStartX,
				(int) (mTextStartX + (1 - mProgress) * mTextWidth));
	}

	private void drawChangeLeft(Canvas canvas) {
		drawText(canvas, mChangeColor, mTextStartX,
				(int) (mTextStartX + mProgress * mTextWidth));
	}

	private void drawOriginLeft(Canvas canvas) {
		drawText(canvas, mOriginColor, (int) (mTextStartX + mProgress
				* mTextWidth), mTextStartX + mTextWidth);
	}

	private void drawText(Canvas canvas, int color, int start, int end) {
		mPaint.setColor(color);
		canvas.save(Canvas.CLIP_SAVE_FLAG);
		if (mDirection == DIRECTION_LEFT || mDirection == DIRECTION_RIGHT) {
			canvas.clipRect(start, 0, end, getMeasuredHeight()); // 在做显示的Canvas中进行裁剪时,你的显示区域将是你的裁剪区域
		} else {
			canvas.clipRect(0, start+(int) (getPaddingTop() / 2), getMeasuredWidth(), end
					+ (int) (getPaddingTop() / 2));// 因为字体的y开始坐标是在字体左下角开始的,而图片是在左上角开始
		}

		canvas.drawText(mText, mTextStartX, mTextStartY, mPaint);
		canvas.restore();
	}

    要注意的文字y的起点坐标是从字的左下角开始的。

android自定义View学习笔记(一)

所以Y的开始坐标是至少大于height(要不然字就显示不全),如果要在水平居中则是 getMeasuredHeight()/2+heigth/2。

还要注意的是canvas的clipRect方法是裁剪出来的区域,就是显示的区域。clipRect(left,top,right,botton)中的参数是含义请看图,如果要裁剪中间绿色区域显示出来就输入以下距离。

android自定义View学习笔记(一)

接下来看看Layout布局文件怎么应用此控件的

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:qiu="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

    <com.example.qiu_myview5_colortrackview.widget.MyColorTrackView
        android:id="@+id/id_my"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:background="#44ff0000"
        android:padding="10dp"
        qiu:progress="0.0"
        qiu:text="你好世界"
        qiu:text_change_color="#ff00ff00"
        qiu:text_origin_color="#ffff0000"
        qiu:text_size="30dip" 
        />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:gravity="center"
        android:orientation="horizontal" >

        <Button
            android:id="@+id/id_left"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="startLeftChange"
            android:text="StartLeft" />

        <Button
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@id/id_left"
            android:layout_weight="1"
            android:onClick="startRightChange"
            android:text="StartRight" />

        <Button
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="startUpChange"
            android:text="Startup" />

        <Button
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:onClick="startDownChange"
            android:text="Startdown" />
    </LinearLayout>

</RelativeLayout>

如果要用自定义的属性例如:qiu:text="你好世界" ,可以再命名空间你加上xmlns:custom="http://schemas.android.com/apk/res-auto" 或http://schemas.android.com/apk/res/项目的package名称。

运行效果:

android自定义View学习笔记(一)


在看看activity的代码

public class MainActivity extends Activity {
	private MyColorTrackView mView;

	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		mView = (MyColorTrackView) findViewById(R.id.id_my);
	}

	@SuppressLint("NewApi")
	public void startLeftChange(View view)
	{
		mView.setDirection(0);
		ObjectAnimator.ofFloat(mView, "progress", 0, 1).setDuration(2000)
				.start();
	}

	@SuppressLint("NewApi")
	public void startRightChange(View view)
	{
		mView.setDirection(1);
		ObjectAnimator.ofFloat(mView, "progress", 0, 1).setDuration(2000)
				.start();
	}
	@SuppressLint("NewApi")
	public void startUpChange(View view)
	{
		mView.setDirection(2);
		ObjectAnimator.ofFloat(mView, "progress", 0, 1).setDuration(2000)
				.start();
	}
	
	@SuppressLint("NewApi")
	public void startDownChange(View view)
	{
		mView.setDirection(3);
		ObjectAnimator.ofFloat(mView, "progress", 0, 1).setDuration(2000)
				.start();
	}
}

原理大致是

用ObjectAnimator.ofFloat(mView, "progress", 0, 1).setDuration(2000).start();来改变自定义控件progress的值,然后不断刷新绘制图(调用canvas方法),要使用ObjectAnimator必须要在自定义方法加上对progress的setProgress()方法,原因是ObjectAnimator用java反射来改变progress的值。

基本算是写完了,第一次写这么多,以后坚持写,虽然写的很烂,相信自己能越写越好。这是一个开始。

你可能感兴趣的:(android,自定义view)