自定义View详解之时钟实战
引言
在我们平时做的项目中,基本上都会用到自定义View来满足我们的页面设计需求,一些基本的知识我们大家都是比较清楚的,可是一些详细的知识,我们可能接触了解的比较少,这次大家就跟一起来熟悉回顾一下吧。
知识前瞻
在我们学习之前我们先可以简答的去看一下View的源码,加上注释之类的,总共是2w多行,有些人看到这个数字就被吓到了,确实View的源码行数是比较多的了,但是里面很多的知识,我们在日常的使用中都已经见过了,我们看起来也不会很累,所以,建议大家还是看一下,加强自己的忍耐力和阅读源码的能力,奥利给!
流程介绍
构造方法
/**
* Simple constructor to use when creating a view from code.
*
* @param context The Context the view is running in, through which it can
* access the current theme, resources, etc.
*/
public View(Context context)
/**
* Constructor that is called when inflating a view from XML. This is called
* when a view is being constructed from an XML file, supplying attributes
* that were specified in the XML file. This version uses a default style of
* 0, so the only attribute values applied are those in the Context's Theme
* and the given AttributeSet.
*
* <p>
* The method onFinishInflate() will be called after all children have been
* added.
*
* @param context The Context the view is running in, through which it can
* access the current theme, resources, etc.
* @param attrs The attributes of the XML tag that is inflating the view.
* @see #View(Context, AttributeSet, int)
*/
public View(Context context, @Nullable AttributeSet attrs)
/**
* Perform inflation from XML and apply a class-specific base style from a
* theme attribute. This constructor of View allows subclasses to use their
* own base style when they are inflating. For example, a Button class's
* constructor would call this version of the super class constructor and
* supply <code>R.attr.buttonStyle</code> for <var>defStyleAttr</var>; this
* allows the theme's button style to modify all of the base view attributes
* (in particular its background) as well as the Button class's attributes.
*
* @param context The Context the view is running in, through which it can
* access the current theme, resources, etc.
* @param attrs The attributes of the XML tag that is inflating the view.
* @param defStyleAttr An attribute in the current theme that contains a
* reference to a style resource that supplies default values for
* the view. Can be 0 to not look for defaults.
* @see #View(Context, AttributeSet)
*/
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr)
/**
* Perform inflation from XML and apply a class-specific base style from a
* theme attribute or style resource. This constructor of View allows
* subclasses to use their own base style when they are inflating.
* <p>
* When determining the final value of a particular attribute, there are
* four inputs that come into play:
* <ol>
* <li>Any attribute values in the given AttributeSet.
* <li>The style resource specified in the AttributeSet (named "style").
* <li>The default style specified by <var>defStyleAttr</var>.
* <li>The default style specified by <var>defStyleRes</var>.
* <li>The base values in this theme.
* </ol>
* <p>
* Each of these inputs is considered in-order, with the first listed taking
* precedence over the following ones. In other words, if in the
* AttributeSet you have supplied <code><Button * textColor="#ff000000"></code>
* , then the button's text will <em>always</em> be black, regardless of
* what is specified in any of the styles.
*
* @param context The Context the view is running in, through which it can
* access the current theme, resources, etc.
* @param attrs The attributes of the XML tag that is inflating the view.
* @param defStyleAttr An attribute in the current theme that contains a
* reference to a style resource that supplies default values for
* the view. Can be 0 to not look for defaults.
* @param defStyleRes A resource identifier of a style resource that
* supplies default values for the view, used only if
* defStyleAttr is 0 or can not be found in the theme. Can be 0
* to not look for defaults.
* @see #View(Context, AttributeSet, int)
*/
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)
View一共四个构造方法,各个构造方面的用途在方法的描述中已经写了,我这里就不再赘述了。
关键方法
想要知道自定义View我们一般常用的那几个关键方法,我们可以简单的想一想。画一个东西,我们需要做哪些操作,测量一下这个物品的大小,物品放在那个位置,物品有哪些内容,物品是否可以移动等等,做完简单的这些,这个物品基本上就被我们画出来了,其他的知识一些细化的东西。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
protected void onLayout(boolean changed, int left, int top, int right, int bottom)
protected void onDraw(Canvas canvas)
下面我们就来详细的讲一讲这三个方法的用法。
onMeasure是测量需要绘制View的大小,好进行下一步View的位置的摆放,具体的调用的位置是在measure,方法里面调用参数分别是父控件对子View的测量宽高的期望,MeasureSpec:父控件对子View的测量宽高的期望———>一个32位的数,前两位表示测量模式——SpecModel;后30位表示测量大小SpecSize。
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
通俗点点讲就是1、Exactly,精确的,有300px,match_parent,2、AtMost,最大是多少,有wrap_content,3、Unspecfide,无限大。测量结束后能获取测量的宽和测量的高,也就是widthSpecSize和heightSpecSize。 通过getMeasureWidth和getMeasureHeight方法。
代表测量结束的方法setMeasureDimetion(widthSpecSize,heightSpecSize)。
自定义ViewGroup一定要重写onMeasure方法,如果不重写则子View获取不到宽和高。重写是在onMeasure方法中调用measureChildern()方法,遍历出所有子View并对其进行测量。
自定义View如果要使用wrap_content属性的话,则需重写onMeasure方法。
onLayout是在确定View的布局的位置的时候调用,方法里面参数的含义分别是,布局是否改变,view的左上右下的位置,具体的调用的位置是在layout方法里面调用,layout方法中调用了setFram(l,t,r,b)方法,该方法内部的实现{mLeft = l;mRight = r;mTop = t;mBottom = b}, 该方法代表布局完成。布局完成之后,能够获取getWidth()和getHeight()的值。这两个方法的实现分别是return mRight - mLeft; return mBottom - mTop;所以有时候我们需要获取View的宽高,我们在onCreate里面获取的时候,我们一般的处理方法就是view.post。 ViewGroup必须实现这个方法,否则子view不能确定其位置。
onDraw方法是进行View的绘制工作,具体的调用位置是在draw方法里面调用,绘制工作一般分为以下几步
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
无论是View或者是ViewGroup都必须实现该方法。
知识拓展
Android屏幕坐标系以及获取各个坐标的含义
Android坐标系是屏幕的左上角是屏幕原点,往右是是X轴正向,往下是Y轴正向,相对于Unity的屏幕中心的是原点,这个要区分,下图可以看出来。
Canvas
顾名思义就是画布,也就是我们将要自定义View的画布,我们需要了解到的是Canvas的一些方法,我们可以参考下面的博客来学一下。
https://www.jianshu.com/p/afa06f716ca6
https://blog.csdn.net/qq_41405257/article/details/80487997
Paint
/*
* Paint类介绍
*
* Paint即画笔,在绘图过程中起到了极其重要的作用,画笔主要保存了颜色,
* 样式等绘制信息,指定了如何绘制文本和图形,画笔对象有很多设置方法,
* 大体上可以分为两类,一类与图形绘制相关,一类与文本绘制相关。
*
* 1.图形绘制
* setARGB(int a,int r,int g,int b);
* 设置绘制的颜色,a代表透明度,r,g,b代表颜色值。
*
* setAlpha(int a);
* 设置绘制图形的透明度。
*
* setColor(int color);
* 设置绘制的颜色,使用颜色值来表示,该颜色值包括透明度和RGB颜色。
*
* setAntiAlias(boolean aa);
* 设置是否使用抗锯齿功能,会消耗较大资源,绘制图形速度会变慢。
*
* setDither(boolean dither);
* 设定是否使用图像抖动处理,会使绘制出来的图片颜色更加平滑和饱满,图像更加清晰
*
* setFilterBitmap(boolean filter);
* 如果该项设置为true,则图像在动画进行中会滤掉对Bitmap图像的优化操作,加快显示
* 速度,本设置项依赖于dither和xfermode的设置
*
* setMaskFilter(MaskFilter maskfilter);
* 设置MaskFilter,可以用不同的MaskFilter实现滤镜的效果,如滤化,立体等 *
* setColorFilter(ColorFilter colorfilter);
* 设置颜色过滤器,可以在绘制颜色时实现不用颜色的变换效果
*
* setPathEffect(PathEffect effect);
* 设置绘制路径的效果,如点画线等
*
* setShader(Shader shader);
* 设置图像效果,使用Shader可以绘制出各种渐变效果
*
* setShadowLayer(float radius ,float dx,float dy,int color);
* 在图形下面设置阴影层,产生阴影效果,radius为阴影的角度,dx和dy为阴影在x轴和y轴上的距离,color为阴影的颜色
*
* setStyle(Paint.Style style);
* 设置画笔的样式,为FILL,FILL_OR_STROKE,或STROKE
*
* setStrokeCap(Paint.Cap cap);
* 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式,如圆形样式
* Cap.ROUND,或方形样式Cap.SQUARE
*
* setSrokeJoin(Paint.Join join);
* 设置绘制时各图形的结合方式,如平滑效果等
*
* setStrokeWidth(float width);
* 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的粗细度
*
* setXfermode(Xfermode xfermode);
* 设置图形重叠时的处理方式,如合并,取交集或并集,经常用来制作橡皮的擦除效果
*
* 2.文本绘制
* setFakeBoldText(boolean fakeBoldText);
* 模拟实现粗体文字,设置在小字体上效果会非常差
*
* setSubpixelText(boolean subpixelText);
* 设置该项为true,将有助于文本在LCD屏幕上的显示效果
*
* setTextAlign(Paint.Align align);
* 设置绘制文字的对齐方向
*
* setTextScaleX(float scaleX);
* 设置绘制文字x轴的缩放比例,可以实现文字的拉伸的效果
*
* setTextSize(float textSize);
* 设置绘制文字的字号大小
*
* setTextSkewX(float skewX);
* 设置斜体文字,skewX为倾斜弧度
*
* setTypeface(Typeface typeface);
* 设置Typeface对象,即字体风格,包括粗体,斜体以及衬线体,非衬线体等
*
* setUnderlineText(boolean underlineText);
* 设置带有下划线的文字效果
*
* setStrikeThruText(boolean strikeThruText);
* 设置带有删除线的效果
*
*/`
项目实战
自定义挂钟View
/**
* Created by Wiky on 2020/10/29
*/
public class ClockView extends View {
private Paint mPaint;
private int mCenterX;
private int mCenterY;
private int mRadius = 500;
private int mLongLine = 60;
private Path mPath;
private Rect mTextBound;
private int mHour;
private int mMinute;
private int mSecond;
private Calendar mCalendar;
public ClockView(Context context) {
super(context);
init(context);
}
public ClockView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context){
DisplayMetrics outMetrics = new DisplayMetrics();
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
windowManager.getDefaultDisplay().getMetrics(outMetrics);
mCenterX = outMetrics.widthPixels/2;
mCenterY = outMetrics.heightPixels/2;
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStrokeWidth(2.0f);
mPaint.setColor(Color.BLACK);
mTextBound = new Rect();
mPath = new Path();
mCalendar = Calendar.getInstance();
mHour = mCalendar.get(Calendar.HOUR);
mMinute = mCalendar.get(Calendar.MINUTE);
mSecond = mCalendar.get(Calendar.SECOND);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setStyle(Paint.Style.STROKE);
//画圆
canvas.drawCircle(mCenterX, mCenterY, mRadius, mPaint);
//画刻度
drawLine(canvas);
//画数字
drawNumber(canvas);
//画圆心
canvas.drawCircle(mCenterX, mCenterY, 10.0f, mPaint);
//画Logo(option)
//画指针
drawTime(canvas);
postDelayed(mRunnable, 1000);
}
/**
* 画刻度
* @param canvas
*/
private void drawLine(Canvas canvas) {
canvas.save();
int startY = mCenterY - mRadius;
int endLongY = startY + mLongLine;
int endShortY = startY + mLongLine / 3;
for (int i = 1; i <= 60; i++) {
canvas.rotate(-6.0f, mCenterX, mCenterY);
if (i % 5 == 0) {
//长刻度
canvas.drawLine(mCenterX, startY, mCenterX, endLongY, mPaint);
} else {
//短刻度
canvas.drawLine(mCenterX, startY, mCenterX, endShortY, mPaint);
}
}
canvas.restore();
}
/**
* 画1-12个数字
* @param canvas
*/
private void drawNumber(Canvas canvas) {
canvas.save();
mPaint.setTextSize(50.0f);
mPaint.setStyle(Paint.Style.FILL);
float offsetY = 10.0f;
for (int j = 1; j <= 12; j++) {
mPaint.getTextBounds(String.valueOf(j), 0, String.valueOf(j).length(), mTextBound);
canvas.rotate(30.0f * j);
float textWidth = mTextBound.width();
float textHeight = mTextBound.height();
float translateY = mRadius - mLongLine - offsetY - textHeight/2;
canvas.translate(0, -translateY);
canvas.rotate(-30.0f * j);
canvas.drawText(String.valueOf(j), -textWidth / 2.0f + mCenterX, mCenterY + textHeight/2, mPaint);
canvas.rotate(30.0f * j);
canvas.translate(0, translateY);
canvas.rotate(-30.0f * j);
}
canvas.restore();
}
/**
* 画时分秒
* @param canvas
*/
private void drawTime(Canvas canvas) {
//画时针
canvas.save();
canvas.rotate(30.0f * mHour + 30.0f/60 * mMinute, mCenterX, mCenterY);
mPath.reset();
mPath.moveTo(mCenterX, mCenterY);
mPath.lineTo(mCenterX+10.0f, mCenterY-50.0f);
mPath.lineTo(mCenterX, mCenterY-250.0f);
mPath.lineTo(mCenterX-10.0f, mCenterY-50.0f);
mPath.lineTo(mCenterX, mCenterY);
canvas.drawPath(mPath, mPaint);
canvas.restore();
//画分针
canvas.save();
canvas.rotate(6.0f * mMinute + 6.0f/60 *
mSecond, mCenterX, mCenterY);
mPath.rewind();
mPath.moveTo(mCenterX, mCenterY);
mPath.lineTo(mCenterX+5.0f, mCenterY-50.0f);
mPath.lineTo(mCenterX, mCenterY-300.0f);
mPath.lineTo(mCenterX-5.0f, mCenterY-50.0f);
mPath.lineTo(mCenterX, mCenterY);
canvas.drawPath(mPath, mPaint);
canvas.restore();
//画秒针
canvas.save();
canvas.rotate(6.0f * mSecond, mCenterX, mCenterY);
mPath.rewind();
mPath.moveTo(mCenterX, mCenterY+30.0f);
mPath.lineTo(mCenterX+3.0f, mCenterY-50.0f);
mPath.lineTo(mCenterX, mCenterY-400.0f);
mPath.lineTo(mCenterX-3.0f, mCenterY-50.0f);
mPath.lineTo(mCenterX, mCenterY+50.0f);
canvas.drawPath(mPath, mPaint);
canvas.restore();
}
/**
* 开启表钟
*/
Runnable mRunnable = new Runnable() {
@Override
public void run() {
mCalendar.setTimeInMillis(System.currentTimeMillis());
mHour = mCalendar.get(Calendar.HOUR);
mMinute = mCalendar.get(Calendar.MINUTE);
mSecond = mCalendar.get(Calendar.SECOND);
postInvalidate();
}
};
}
在此项目中比较难的一个点就是画1-12这12个数字,首先我们需要了解一下Text的一些尺寸参数
Paint mPaint = new Paint();
mPaint.setTextSize(50);
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
float ascent = fontMetrics.ascent;
float bottom = fontMetrics.bottom;
float descent = fontMetrics.descent;
float leading = fontMetrics.leading;
float top = fontMetrics.top;
总结
经过这个简单的时钟自定义View,我们算是比较简单的了解了一些自定义View的过程,以及Canvas、Paint等的一些基础知识,如果需要较深入的了解,还需要平常多使用以及其他的自定义View的设计,多用多实践才是王道,大家一起加油!
本文地址:https://blog.csdn.net/u010887088/article/details/109640203