自己也学Android不久,就几个月的时间,自己做编程也有两年多的时间,都是在一些小公司,而还比较清闲基本一一周的实际工作两三天就搞定了,所以进步不是很大。看着同学的工资蹭蹭的往上涨,而自己拿的工资也是那么一点点。看别人博客说写博客是一个很好进步技术的方式,于是我也来尝试。之前都是转载别人的博客内容,自己实际上写得很少。不扯淡了,本篇文章是学习了鸿洋大神博客学到,自己也来学习写写。
分析出自定义view所需要的属性
继承view在钩针器中获取自己定义的属性
重写view中onMeasure()方法
重写view中的onDraw()方法
基本上是这四个步骤, 其中onMeasure()方法主要是用于计算view空间的宽高的,所以不一定必须重写(如果不重写默认是占满全屏)。onDraw()方法主要是绘制view。
具体讲解一下步骤,现在Android工程的/res/values新建一个名为attrs.xml的文件,此文件就是用来定义自定义属性的,Android是用attr表现自定义属性。
<?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的起点坐标是从字的左下角开始的。
所以Y的开始坐标是至少大于height(要不然字就显示不全),如果要在水平居中则是 getMeasuredHeight()/2+heigth/2。
还要注意的是canvas的clipRect方法是裁剪出来的区域,就是显示的区域。clipRect(left,top,right,botton)中的参数是含义请看图,如果要裁剪中间绿色区域显示出来就输入以下距离。
接下来看看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名称。
运行效果:
在看看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的值。
基本算是写完了,第一次写这么多,以后坚持写,虽然写的很烂,相信自己能越写越好。这是一个开始。