您的位置:首页 > 移动开发 > Android开发

Android 高级自定义View实战

2017-08-20 00:00 344 查看
摘要: 对于android开发来说,自带的组件View往往满足不了UI设计师们的各种天马行空的幻想,所以这就要求我们要有自定义view的能力.

在android组件中主要分为两种:容器(LinearLayout....)和子View(TextView......),但是这些现有的组件往往不能满足app的开发。比如实现一个流式标签,炫酷的进度条显示呢。都需要我们探寻源码,分析和改造成我们想要的效果。这次的主题先从View的自定义入手。



概述:

针对View的自定义主要从 onMeasure()和onDraw()这两个方法入手。
onMeasure():测量View的大小
测量view只要针对我们在xml中wrap_content和match_parent的两种属性。而在onMeasure()对 应的字段是AT_MOST 和EXACTLY。Android默认的实现了EXACTLY的测量也就是精准测量(对应xml属性 match_parent和具体值),对于AT_MOST默认填充父容器,如果要实现包裹那就需要我们自己动手丰衣 足食了。

onDraw():绘制内容,比如形状,图片啊都在这里实现。
主要用到类有paint(画笔),canvas(画板)由这两个类,我们就可以随心所欲的画画l咯。

实践出真理:

“纸上得来终觉浅,绝知此事要躬行“,现在我们就一步一步的用代码来分析自定义View的实践。想了好久我要用啥一个相对简单但大家都熟悉的view作为本文的的入门demo呢?最后决定我们来实现android自带的TextView。

入门demo:

创建一个class类,名为MyTextView 继承View。

package huangzhibo.com.learndemo.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

import huangzhibo.com.learndemo.R;

/**
* Created by HuangZhiBo on 2017/8/20/020.
*/

public class MyTextView extends View {
private String text;
private int paintColor = Color.BLACK;

public MyTextView(Context context) {
super(context);
}

public MyTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/**实现AT_MOST的测量*/
}

public void setText(String info) {
text = info;
}

public void setTextColor(int color) {
paintColor = color;
}

@Override
protected void onDraw(Canvas canvas) {
if (text == null) {  //文本为空
return;
}
Paint paint = new Paint();
paint.setColor(paintColor);
paint.setAntiAl
7fe0
ias(true); //抗锯齿
paint.setTextSize(30);  //文本大小
canvas.drawText(text,this.getWidth()/2-getTextWidth(paint,text)/2,this.getHeight()/2,paint);
}

/**
* 测量文字宽度
* @param paint
* @param str
* @return
*/
public static int getTextWidth(Paint paint, String str) {
int w= 0;
if (str != null && str.length() > 0) {
int len = str.length();
float[] widths = new float[len];
paint.getTextWidths(str, widths);
for (int j = 0; j < len; j++) {
w+= (int) Math.ceil(widths[j]);
}
}
return w;
}
}

xml布局

<?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:orientation="vertical"
android:layout_height="match_parent"
tools:context="huangzhibo.com.learndemo.activity.CustomView">

<huangzhibo.com.learndemo.view.MyTextView
android:background="@color/colorAccent"
android:id="@+id/myTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>

代码都有相应的注释,这里就不在废话,这里对于onMeasure并没有处理,采用默认。注意看layout_width和layout_height的属性值为wrap_content。上文我说道,如果对于onMearsure并未处理,那么view将填充父容器。为了看清楚边界,view设置了一个背景。结果不出所料,那么接下我们就来实现Android对于wrap_content的测量。

效果图:



在对于onMeasure的AT_MOST,我查了下网上很多资料都是直接给个固定值,然后通过比较返回最大值,这样根本就是治标不治本,我们要的效果是完全的TextView效果。那么问题来了,要实现包裹效果,也就需要确定宽和高。而这个宽和高就是文本的宽和高加上内边距(pading)。思路有了那就好办了。

package huangzhibo.com.learndemo.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

import huangzhibo.com.learndemo.R;

import static android.os.Build.VERSION_CODES.M;

/**
* Created by HuangZhiBo on 2017/8/20/020.
*/

public class MyTextView extends View {
private String text;
private int paintColor = Color.BLACK;
private Paint mPaint;

public MyTextView(Context context) {
super(context);
}

public MyTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint=new Paint();
mPaint.setColor(paintColor);
mPaint.setAntiAlias(true); //抗锯齿
mPaint.setTextSize(30);  //文本大小
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int result = 200;
/**实现AT_MOST的测量*/
setMeasuredDimension(meaSureWidth(widthMeasureSpec),meaSureHeight(heightMeasureSpec));
}

/**
* 测量宽度
* @param measureSpec
* @return
*/
private int meaSureWidth(int measureSpec){
int result=0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode==MeasureSpec.AT_MOST){
/**文本宽度+左右内边距*/
int v = (int)mPaint.measureText(text) + getPaddingLeft() + getPaddingRight();
result= Math.min(v, specSize);
}
return result;
}
/**
* 测量宽度
* @param measureSpec
* @return
*/
private int meaSureHeight(int measureSpec){
int result=0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode==MeasureSpec.AT_MOST){
/**文本高度+上下内边距*/
int v = (int) (-mPaint.ascent() + mPaint.descent())  + getPaddingTop() + getPaddingBottom();
result= Math.min(v, specSize);
}
return result;
}

public void setText(String info) {
text = info;
}

public void setTextColor(int color) {
paintColor = color;
}

@Override
protected void onDraw(Canvas canvas) {
if (text == null) {  //文本为空
return;
}
/**注意 drawText中的x,y分别指的是文字的左边位置,文字baseLine的位置*/
canvas.drawText(text,getPaddingLeft(),this.this.getHeight()-mPaint.descent()-getPaddingBottom(),mPaint);
}

/**
* 测量文字宽度
* @param paint
* @param str
* @return
*/
public static int getTextWidth(Paint paint, String str) {
int w= 0;
if (str != null && str.length() > 0) {
int len = str.length();
float[] widths = new float[len];
paint.getTextWidths(str, widths);
for (int j = 0; j < len; j++) {
w+= (int) Math.ceil(widths[j]);
}
}
return w;
}
}

效果图:



到这里,我们已经完全解决了AT_MOST的测量了。不过对于以上的代码还需要解释下:

(-mPaint.ascent() + mPaint.descent()) 这个为啥能得到文字高度?原理可以看下这篇文章
http://blog.csdn.net/mq2856992713/article/details/52327938(这个必须掌握)

基本的自定义view,我相信到这里你已经差不多掌握了,那么如何达到炉火纯青,信手拈来的境界呢?没有捷径,就是熟能生巧,接下来实现几个效果View。

仿华为记步View和常见的验证码View:





华为记步View代码如下:
package huangzhibo.com.learndemo.view;

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import huangzhibo.com.learndemo.R;

/**
* Created by HuangZhiBo on 2017/7/31/031.
*/

public class StepView extends View {
/*圆弧宽度*/
private float borderWidth = 38f;
/* 画步数的数值的字体大小*/
private float numberTextSize = 0;
/**
* 开始绘制圆弧的角度
*/
private float startAngle = 135;
/**
* 终点对应的角度和起始点对应的角度的夹角
*/
private float angleLength = 270;
/**
* 所要绘制的当前步数的红色圆弧终点到起点的夹角
*/
private float currentAngleLength = 0;
private String stepNumber;
/**
* 动画时长
*/
private int animationLength = 3000;

public StepView(Context context) {
super(context);
}

public StepView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

public StepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/*中心点坐标*/
float centerX = (getWidth()) / 2;
/*指定圆弧的外轮廓矩形区域*/
RectF rectF = new RectF(0 + borderWidth, borderWidth, 2 * centerX - borderWidth, 2 * centerX - borderWidth);
/*绘制红色圆弧*/
drawArcYellow(canvas, rectF);
/*绘制蓝色走过步数*/
drawArcRed(canvas, rectF);
/*文字*/
drawTex(canvas, rectF);
}

private void drawTex(Canvas canvas, RectF rectF) {
Paint paint = new Paint();
paint.setColor(getResources().getColor(R.color.colorAccent));
paint.setAntiAlias(true);
paint.setTextSize(32);
int length = String.valueOf(currentAngleLength).length();
canvas.drawText(currentAngleLength + "", rectF.centerX() - 70, getHeight() / 2, paint);
}

/**
* 1.绘制总步数的黄色圆弧
*
* @param canvas 画笔
* @param rectF  参考的矩形
*/
private void drawArcYellow(Canvas canvas, RectF rectF) {
Paint paint = new Paint();
paint.setColor(getResources().getColor(R.color.colorAccent));
/** 结合处为圆弧*/
paint.setStrokeJoin(Paint.Join.ROUND);
/** 设置画笔的样式 Paint.Cap.Round ,Cap.SQUARE等分别为圆形、方形*/
paint.setStrokeCap(Paint.Cap.ROUND);
/** 设置画笔的填充样式 Paint.Style.FILL  :填充内部;Paint.Style.FILL_AND_STROKE  :填充内部和描边;  Paint.Style.STROKE  :仅描边*/
paint.setStyle(Paint.Style.STROKE);
/**抗锯齿功能*/
paint.setAntiAlias(true);
/**设置画笔宽度*/
paint.setStrokeWidth(38f);
canvas.drawArc(rectF, startAngle, angleLength, false, paint);
}

/**
* 2.绘制当前步数的蓝色圆弧
*
* @param canvas
* @param rectF
*/
private void drawArcRed(Canvas canvas, RectF rectF) {
Paint paint = new Paint();
/**设置结合处的样子,Miter:结合处为锐角, Round:结合处为圆弧:BEVEL:结合处为直线。*/
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStyle(Paint.Style.STROKE);//设置填充样式
/*** 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式,如圆形样式
Cap.ROUND,或方形样式Cap.SQUARE   */
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setAntiAlias(true);//抗锯齿功能
paint.setStrokeWidth(borderWidth);//设置画笔宽度
paint.setColor(getResources().getColor(R.color.colorPrimary));
canvas.drawArc(rectF, startAngle, currentAngleLength, false, paint);

}

/**
* 所走的步数进度
*
* @param totalStepNum  设置的步数
* @param currentCounts 所走步数
*/
public void setCurrentCount(int totalStepNum, int currentCounts) {
stepNumber = currentCounts + "";
/**如果当前走的步数超过总步数则圆弧还是270度,不能成为园*/
if (currentCounts > totalStepNum) {
currentCounts = totalStepNum;
}
/**所走步数占用总共步数的百分比*/
float scale = (float) currentCounts / totalStepNum;
/**换算成弧度最后要到达的角度的长度-->弧长*/
float currentAngleLength = scale * angleLength;
/**开始执行动画*/
setAnimation(0, currentAngleLength, animationLength);
}

/**
* 为进度设置动画
* ValueAnimator是整个属性动画机制当中最核心的一个类,属性动画的运行机制是通过不断地对值进行操作来实现的,
* 而初始值和结束值之间的动画过渡就是由ValueAnimator这个类来负责计算的。
* 它的内部使用一种时间循环的机制来计算值与值之间的动画过渡,
* 我们只需要将初始值和结束值提供给ValueAnimator,并且告诉它动画所需运行的时长,
* 那么ValueAnimator就会自动帮我们完成从初始值平滑地过渡到结束值这样的效果。
*
* @param last
* @param current
*/
private void setAnimation(float last, float current, int length) {
ValueAnimator progressAnimator = ValueAnimator.ofFloat(last, current);
progressAnimator.setDuration(length);
progressAnimator.setTarget(currentAngleLength);
progressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentAngleLength = (float) animation.getAnimatedValue();
invalidate();
}
});
progressAnimator.start();
}
}

代码里面也注释很多了,这里就不在过多解释

常见的验证码View代码:

package huangzhibo.com.learndemo.utils.commonedite;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.ColorRes;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.AppCompatEditText;
import android.text.Editable;
import android.text.TextPaint;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.WindowManager;

import huangzhibo.com.learndemo.R;

/**
* Created by HuangZhiBo on 2017/7/11/011.
*/

public class VerificationCodeEditText extends AppCompatEditText implements VerificationAction, TextWatcher {
private int mFigures;
private int mVerCodeMargin;
private int mBottomSelectedColor; //底部选种颜色
private int mBottomNormalColor;  //未选中颜色
private float mBottomLineHeigth; //底部高度
private int mSeleceBackgroundColor; //选中的背景颜色

private OnVerificationCodeChangedListener onCodeChangeListener;
private int mCurrentPosition = 0;
private int mEachRectLength = 0;
private Paint mSelectBackGroundPaint;
private Paint mNormalBackGroundPaint;
private Paint mBottomSelectdPaint;
private Paint mBottomNormalPaint;

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

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

public VerificationCodeEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAttrs(attrs);
setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent)); //防止出现下划线
initPaint();
setFocusableInTouchMode(true);
super.addTextChangedListener(this);
}

/**
* 初始化pain
*/
private void initPaint() {
mSelectBackGroundPaint = new Paint();
mSelectBackGroundPaint.setColor(mSeleceBackgroundColor);
mNormalBackGroundPaint = new Paint();
mNormalBackGroundPaint.setColor(getColor(android.R.color.transparent));

mBottomSelectdPaint = new Paint();
mBottomSelectdPaint.setColor(mBottomSelectedColor);
mBottomNormalPaint = new Paint();
mBottomNormalPaint.setColor(mBottomNormalColor);
mBottomSelectdPaint.setStrokeWidth(mBottomLineHeigth);
mBottomNormalPaint.setStrokeWidth(mBottomLineHeigth);

}

private void initAttrs(AttributeSet attrs) {
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.EditText);
mFigures = ta.getInteger(R.styleable.EditText_figures, 4);
mVerCodeMargin = (int) ta.getDimension(R.styleable.EditText_verCodeMargin, 10);
mBottomSelectedColor = ta.getColor(R.styleable.EditText_bottomLineSelectedColor, getCurrentTextColor());
mBottomNormalColor = ta.getColor(R.styleable.EditText_bottomLineNormalColor, getColor(android.R.color.holo_red_dark));
mBottomLineHeigth = ta.getDimension(R.styleable.EditText_bottomLineHeight, dp2px(5));
mSeleceBackgroundColor = ta.getColor(R.styleable.EditText_selectedBackgroundColor, getColor(android.R.color.holo_red_dark));
ta.recycle();
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthResult = 0, heighResult = 0;
//最终宽度
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
widthResult = widthSize;
} else {
widthResult = getScreenWidth(getContext());
}
//每个矩形的宽度
mEachRectLength = (widthResult - (mVerCodeMargin * (mFigures - 1))) / mFigures;
//最终高度
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode == MeasureSpec.EXACTLY) {
heighResult = heightSize;
} else {
heighResult = mEachRectLength;
}
setMeasuredDimension(widthResult, heighResult);
}

@Override
protected void onDraw(Canvas canvas) {
mCurrentPosition = getText().length();
int width = mEachRectLength - getPaddingLeft() - getPaddingRight(); //每个矩形宽度
int height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); //整体高度
//绘制每个矩形
for (int i = 0; i < mFigures; i++) {
canvas.save();
int start = width * i + i * mVerCodeMargin;
int end = width + start;
//画一个矩形
if (i == mCurrentPosition) {//选中
canvas.drawRect(start, 0, end, height, mSelectBackGroundPaint);
} else {
canvas.drawRect(start, 0, end, height, mNormalBackGroundPaint);
}
canvas.restore();
}
//绘制文字
String value = getText().toString();
for (int i = 0; i < value.length(); i++) {
canvas.save();
int start = width * i + i * mVerCodeMargin;
float x = start + width / 2;
TextPaint paint = getPaint();
paint.setTextAlign(Paint.Align.CENTER);
paint.setColor(getCurrentTextColor());
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
float baseLine = (height - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top;//top 是个负数
canvas.drawText(String.valueOf(value.charAt(i)), x, baseLine, paint);
canvas.restore();
}
//绘制底线
for (int i = 0; i < mFigures; i++) {
canvas.save();
float lineY = height - mBottomLineHeigth / 2;
int start = width * i + i * mVerCodeMargin;
int end = width + start;
if (i < mCurrentPosition) {
canvas.drawLine(start, lineY, end, lineY, mSelectBackGroundPaint);
} else {
canvas.drawLine(start, lineY, end, lineY, mBottomNormalPaint);
}
canvas.restore();
}
}

/**
* 获取手机屏幕的宽度
*/
static int getScreenWidth(Context context) {
DisplayMetrics metrics = new DisplayMetrics();
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
wm.getDefaultDisplay().getMetrics(metrics);
return metrics.widthPixels;
}

@Override
final public void setCursorVisible(boolean visible) {
super.setCursorVisible(false);//隐藏光标的显示
}

@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
mCurrentPosition = getText().length();
postInvalidate();
}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
mCurrentPosition = getText().length();
postInvalidate();
if (onCodeChangeListener != null) {
onCodeChangeListener.onVerCodeChanged(getText(), start, before, count);
}
}

@Override
public void afterTextChanged(Editable s) {
mCurrentPosition = getText().length();
postInvalidate();
if (getText().length() == mFigures) {
if (onCodeChangeListener != null) {
onCodeChangeListener.onInputCompleted(getText());
}
} else if (getText().length() > mFigures) {
getText().delete(mFigures, getText().length());
}
}

@Override
public void setFigures(int figures) {
mFigures = figures;
postInvalidate();
}

@Override
public void setVerCodeMargin(int margin) {
mVerCodeMargin = margin;
postInvalidate();
}

@Override
public void setBottomSelectedColor(@ColorRes int bottomSelectedColor) {
mBottomSelectedColor = bottomSelectedColor;
postInvalidate();
}

@Override
public void setBottomNormalColor(@ColorRes int bottomNormalColor) {
mBottomNormalColor = bottomNormalColor;
postInvalidate();
}

@Override
public void setSelectedBackgroundColor(@ColorRes int selectedBackground) {
mSeleceBackgroundColor = selectedBackground;
postInvalidate();
}

@Override
public void setBottomLineHeight(int bottomLineHeight) {
mBottomLineHeigth=bottomLineHeight;
postInvalidate();
}

@Override
public void setOnVerificationCodeChangedListener(OnVerificationCodeChangedListener listener) {
this.onCodeChangeListener=listener;
}

/**
* 返回颜色
*/
private int getColor(@ColorRes int color) {
return ContextCompat.getColor(getContext(), color);
}

/**
* dp转px
*/
private int dp2px(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
getResources().getDisplayMetrics());
}

}

对于自定义验证码的View,需要考虑超出文字字数处理,文字变化监听处理,相对于这个demo来说还是有一点难度,需要大家慢慢的去理解和消化。这里把几个要点总结下,以便更好的去理解这个实现逻辑

初始界面,绘画一个矩形(分为选中和未选中),底部横线,绘制文字

文字输入监听,重新绘制

文字超出处理

以上的demo已经放在网上仓库,需要的话可以点击https://git.oschina.net/huagnzhibo123/LearnDemo

这个demo有我最近在整理的项目常用的工具类和框架,会不断完善。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息