自定义View的基本流程

有时候我们需要一些特殊的效果或者功能,而系统控件无法满足我们的需求,这时候就需要自己定义一个控件。

因为工作原因,想写一篇自定义view的初级心得。一、一般而言写自定义view有大体6个步骤:

View的工作流程主要指的是measure、Layout、draw三个流程,即测量、布局、绘制。measure测量view的宽高,Layout确定view的宽高和四个顶点,draw绘制到屏幕上。

澳门威斯尼人平台登录 1

1.明确需求,确定你想实现的效果
2.确定是使用组合控件的形式还是全新自定义的形式,组合控件即使用多个系统控件来合成一个新控件,你比如titilebar,这种形式相对简单,参考:
3.如果是完全自定义一个view的话,你首先需要考虑继承哪个类,是View呢,还是ImageView等子类
4.根据需要去复写View#onDraw、View#onMeasure、View#onLayout方法
5.根据需要去复写dispatchTouchEvent、onTouchEvent方法
6.根据需要为你的自定义view提供自定义属性,即编写attr.xml,然后在代码中通过TypedArray等类获取到自定义属性值
7.需要处理滑动冲突、像素转换等问题

继承View

要自定义View首先需要需要继承View或者其子类,如果需要实现的效果比较复杂,通常需要继承View,有时候我们需要的是系统的控件再加上一些特殊的效果则可以继承View的子类(如TextView)

如果是要自己设计一种布局或者要组合其他控件,这时候就需要继承ViewGroup或者LinearLayout、FrameLayout等系统自带的布局

  1. 继承View的某个子类,包括ViewGroup的子类(毕竟ViewGroup也是View的子类嘛╮ 2.
    重写继承的父类View的一些特定函数及常用的三个:(测量measure),,3.为自定义View类增加属性(主要是在那三个重写的构造方法里)4.绘制控件5.响应用户事件(单击、输入文字、触摸、滑动等等~~)6.定义回调函数二、针对继承对象的不同自定义View分为继承View
    与ViewGroup两种的情况,我上面2里的所说的常用三个使用上有所区别。测量measure:View:普通View的onMeasure逻辑大同小异,基本都是测量自身内容和背景,然后根据父View传递过来的MeasureSpec进行最终的大小判定,例如TextView会根据文字的长度,文字的大小,文字行高,文字的行宽,显示方式,背景图片,以及父View传递过来的模式和大小最终确定自身的大小。具体的View宽高测量是调用了
    setMeasuredDimension() 方法:

自定义View答题分为四类:

上面每一个item,看上去好像都是一个horizontal的LinearLayout装载了两个ImageView,一个textView,但是这样写未免太麻烦了,这个布局可以只用一个textview实现,但是如果自定义一个这样的view该怎么写呢。

重写构造方法

自定义View至少需要重写两个构造方法

  • 在java代码中直接创建控件所用的构造方法

public CustomView(Context context) { this(context, null);}
  • 在xml文件中定义View所用的构造函数

public CustomView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0);}public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mPaint = new Paint();}

1、继承View重写onDraw方法

首先建一个class,继承LinearLayout并给出构造方法:

自定义xml中的属性

首先需要新建res/values/custom_view_attrs.xml,并在里面声明如下

<?xml version="1.0" encoding="utf-8"?><resources> <declare-styleable name="CustomView"> <attr name="custom_width" format="dimension" /> </declare-styleable></resources>

然后就可以在xml布局文件中声明了

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:andro xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="cn.lkllkllkl.customview.MainActivity"> <cn.lkllkllkl.customview.CustomView android:layout_width="wrap_content" android:layout_height="wrap_content" app:custom_width="100dp"/></LinearLayout>

接着我们就可以在自定义View的构造方法中将该属性的值取出使用了

public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mPaint = new Paint(); float customWidth = 0; TypedArray typeArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView); customWidth = typeArray.getDimension(R.styleable.CustomView_custom_width, 100); // 需要记得回收 typeArray.recycle(); }
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } 

这种方法主要自定义一些不规则的效果,即这种效果不方便通过布局的组合方式实现,用这种方式需要自己支持wrap_content,并且支持padding也需要自己处理

“`

onMeasure方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec); 

这里要明白onMeasure的两个参数首先需要明白MeasureSpec这个类。

澳门威斯尼人平台登录,MeasureSpec通过将SpecMode和SpecSize打包成一个int(即widthMeasureSpec、heightMeasureSpec),前2位表示SpecMode,后30位表示尺寸。

SpecMode有三类

  • UNSPECIFIED:
    父容器对View的大小没有限制,一般我们自定义View用的较少

  • EXACTLY:
    在xml文件中设置具体大小为多少dp,或者设置为match_parent则SpecMode为EXACTLY,此模式表示则直接使用SpecSize作为自定义View的尺寸

  • AT_MOST:
    对应xml文件中的wrap_content,表示设置的尺寸大小不能超过SpecSize

通常我们需要view支持wrap_content的时候才需要重写onMeasure,如果不重写则wrap_content效果和match_parent一样。一般使用如下代码就够了

 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); // dp转换成px, 事先定义的默认宽高单位为dp int defaultWidth = dp2px(getContext(), DEFAULT_WIDTH); int defaultHeight = dp2px(getContext(), DEFAULT_HEIGHT); if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) { widthSize = Math.min(defaultWidth, widthSize); heightSize = Math.min(defaultHeight, heightSize); } else if (widthMode == MeasureSpec.AT_MOST) { widthSize = Math.min(defaultWidth, widthSize); } else if (heightMode == MeasureSpec.AT_MOST){ heightSize = Math.min(defaultHeight, heightSize); } setMeasuredDimension(widthSize, heightSize); }

一般为了适配不同屏幕需要使用dp为单位,而setMeasuredDimension是使用px为单位的,所以上面的代码将我们设置的默认宽高转换成px,转换方法如下

public static int dp2px(Context context, float dp) { return  TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics; }

onMeasure通过父View传递过来的大小和模式,以及自身的背景图片的大小得出自身最终的大小,通过setMeasuredDimension()方法设置给mMeasuredWidth和mMeasuredHeight。
ViewGroup:ViewGroup本身没有实现onMeasure(但是!有setMeasuredDimension,但是他的子类(比如:四大布局控件)都有各自的实现,通常他们都是通过measureChildWithMargins()这种测量内部子view的方法来遍历内部,测量子View。当所有的子View都测量完毕后,才根据父View传递过来的模式和大小来最终决定自身的大小。**
注意事项:如果子View被GONE的将不参与测量。**ViewGroup一般都在测量完所有子View后才会调用setMeasuredDimension()设置自身大小。经过measure
完成后,我们就可以通过getMeasuredWidth/Height 获取View 的宽高。
放置layout:View:普通View中的onLayout()这个函数为空函数。所以不用理会,想想也是的吧,如果你继承的是view,你还有摆放你里面的内容吗?如果里面有东西需要你的摆放,那么,这个view不就是父view了!这个不就该是继承的是ViewGroup。好的,往下看。ViewGroup:对于ViewGroup而言,循环遍历所有子View是主要的思想!!!因此如果我们继承ViewGroup
我们需要遍历执行所有的child.layout()。Layout方法中接受四个参数,是由父View提供,指定了子View在父View中的左、上、右、下的位置。父View在指定子View的位置时通常会根据子View在measure中测量的大小来决定。注意事项:子View的位置通常还受到父View的orientation,gravity,padding,子View的margin等等属性的影响哦,我相信写过在xml写过布局的各位大大肯定是了解的吧。ViewGroup中的onLayout()方法:

2、继承ViewGroup派生出特殊的Layout

class SettingItemLayout extends LinearLayout{

onLayout方法

一般自定义ViewGroup才需要重写该方法对子View进行排放

@Override protected abstract void onLayout(boolean changed, int l, int t, int r, int b); 

这种方法主要用于实现特殊的布局,即除了LinearLayout、RelativeLayout、FrameLayout这几种系统布局之外,我们重新定义的一种新布局。采用这种方式比较复杂一些,需要适当测处理vIewGroup的量值和布局这两个过程并同时处理子元素的测量和布局过程。

public SettingItemLayout(Context context,@NullableAttributeSet attrs)
{

onDraw方法

通过onDraw方法我们可以将view绘制到屏幕上,如果要让view支持padding属性则需要在onDraw中做处理

@Override protected void onDraw(Canvas canvas) { super.onDraw; /* mPaint为成员变量,画笔Paint的对象, * 在构造函数中进行初始化,可以设置一些在canvas上绘制的通用属性 */ mPaint.setColor(Color.RED); int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingRight(); int paddingTop = getPaddingTop(); int paddingBottom = getPaddingBottom(); //在View所占区域绘制一条对角线 canvas.drawLine(paddingLeft, paddingTop, getWidth() - paddingRight, getHeight() - paddingBottom, mPaint); }
  • onMeasure、onLayout、onDraw三个方法可能会对此调用,特别是onDraw方法,所以最好不要在这些方法中创建对象避免频繁分配内存造成内存抖动,或者做一些耗时操作导致跳帧

  • View中有提供post系列方法,所以不需要在View中使用Handler

  • View中若有创建线程或者动画需要及时停止,View#onDetachedFromWindow就是一个很好的时机

  • View中若有滑动嵌套的情形,需要处理好滑动冲突

  • 《Android开发艺术探索》

  • 自定义View,有这一篇就够了

抽象就表示了继承ViewGroup的子类布局控件,都要去重写。而这个重写也就导致了,不同的布局方式。怎么重写呢?举个例子:我这里将第一个子控件通过layout()放置到左上角0,0
宽高是测量值。

3、继承特定的view(比如TextView)

super(context,attrs);

 @Override protected void onLayout(boolean changed, int l, int t, int r, int b) {View childView = getChildAt; childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight; } 

这种比较常见,一般用于扩展某种已有的View功能比如TextView,这种方法比较容易实现,也不需要自己支持warp_content和padding等。

initLayout(context,attrs);//这个方法用来初始化布局

绘制draw
draw()的过程就是绘制View到屏幕上的过程,draw()的执行遵循如下步骤:

4、继承特定的ViewGroup(比如 LinearLayout)

}

  1. 绘制背景2.保存画布的图层来准备色变
  2. 绘制内容4.绘制children5.画出褪色的边缘和恢复层
  3. 绘制装饰 比如scollbar2和5
    可以跳过的。View:view中onDraw()是个空函数,也就是说需要每个视图根据想要展示的内容来自行绘制,View是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制。如果你去观察TextView、ImageView等类的源码,你会发现它们都有重写onDraw()这个方法,并且在里面执行了相当不少的绘制逻辑:
    在TextView中在该方法中绘制文字、光标和CompoundDrawable;ImageView中相对简单,只是绘制了图片。绘制的方式主要是借助Canvas这个类,它会作为参数传入到onDraw()方法中,供给每个视图使用。Canvas这个类的用法非常丰富,基本可以把它当成一块画布,在上面绘制任意的东西,那么我们就来尝试一下吧。澳门威斯尼人平台登录 2

这种方法比较常见,当某种效果看起来像几种View组合在一起的时候可以采用这种方式来实现。采用这种方式不需要自己处理ViewGroup的测量和布局这两个过程,一般方式2能实现的效果这个方式都能实现,两者的区别在于方式2更接近View底层。

}

View
的绘制主要通过dispatchDraw(),先根据自身的padding剪裁画布,所有的子View都将在画布剪裁后的区域绘制。遍历所有子View,调用子View的computeScroll对子View的滚动值进行计算。根据滚动值和子View在父View中的坐标进行画布原点坐标的移动,根据子在父View中的坐标计算出子View的视图大小,然后对画布进行剪裁,请看下面的示意图。ViewGroup:对于ViewGroup则不需要实现该函数,因为作为容器是“没有内容“的(但必须ViewGroup要有实现dispatchDraw()函数,告诉子view去绘制自己)。注意事项:dispatchDraw的逻辑其实比较复杂,但ViewGroup已经处理好了,我们不必要重载该方法对子View进行绘制事件的派遣分发。三、其他一些可以用来重写的方法:onTouchEvent定义触屏事件来响应用户操作。
onKeyDown 当按下某个键盘时onKeyUp 当松开某个键盘时onTrackballEvent
当发生轨迹球事件时onSizeChange() 当该组件的大小被改变时onFinishInflate()
回调方法,当应用从XML加载该组件并用它构建界面之后调用的方法onWindowFocusChanged
当该组件得到、失去焦点时onAttachedToWindow()
当把该组件放入到某个窗口时onDetachedFromWindow()
当把该组件从某个窗口上分离时触发的方法onWindowVisibilityChanged:
当包含该组件的窗口的可见性发生改变时触发的方法

“`

四、View的绘制流程绘制流程函数调用关系如下图:

这时候就要考虑自定义的这个view里面都有些啥东西:

澳门威斯尼人平台登录 3这里写图片描述

“`

五:requestLayout() 、invalidate()、postInvalidate()requestLayout():
当view确定自身已经不再适合现有的区域时,该view本身调用requestLayout()方法来要求parent
view重新调用他的measure和layout来重新设置自己位置。特别是当view的layoutparameter发生改变,并且它的值还没能应用到view上时,这时候适合调用这个方法。注意,并不会不执行ondraw。

ImageView imgLeft;//左边的图标

invalidate()、postInvalidate(): 调用invalidate()、postInvalidate()会
界面刷新,执行 draw
过程。区别就是Invalidate不能直接在线程中调用,因为他是违背了单线程模型:Android
UI操作并不是线程安全的,并且这些操作必须在UI线程中调用。
鉴于此,如果要使用invalidate的刷新,那我们就得配合handler的使用,使异步非ui线程转到ui线程中调用,如果要在非ui线程中直接使用就调用postInvalidate方法即可,这样就省去使用handler的烦恼。

TextView tv;//文字

六、自定义控件的三种方式1、
继承已有的控件当要实现的控件和已有的控件在很多方面比较类似,
通过对已有控件的扩展来满足要求。即:继承TextView、Button这样已有的View(包括项目里已有的自定义View)。2、
继承一个布局文件一般用于自定义组合控件,在构造函数中通过inflater和addView()方法加载自定义控件的布局文件形成图形界面(不需要onDraw方法),就好像是把activity的xml变成用自定义view的xml来表示。3、继承view通过onDraw方法来绘制出组件界面。即继承View,得到和TextView、Button这样等级的View

privateImageViewimg_tip;//右边的跳转图标

七、自定义属性的两种方法1、在布局文件中直接加入属性,在构造函数中去获得。布局文件:

“`

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:andro android:layout_width="match_parent" android:layout_height="match_parent" > <rcjs.com.customview.ZYView android:layout_width="wrap_content" android:layout_height="wrap_content" Text="rcjs" /></RelativeLayout>

有了上面三个变量,这个布局就成型了,但是左边这个图标需要我们动态设置,所以还要声明一个变量用来从外部设置图片资源

获取属性值:

“`

 public ZYView(Context context, AttributeSet attrs) { super(context, attrs); int textId = attrs.getAttributeResourceValue(null, "Text", 0); String text = context.getResources().getText.toString(); }

Drawable drawable;//设置的图片标资源

2、在res/values/ 下建立一个attrs.xml
来声明自定义view的属性。
可以定义的属性有:

private intmHeight,mWidth;//图标的宽、高

<declare-styleable name="名称">//参考某一资源ID (name可以随便命名)<attr name="background" format="reference"/>//颜色值<attr name="textColor" format="color"/>//布尔值<attr name="focusable" format="boolean"/>//尺寸值<attr name="layout_width" format="dimension"/>//浮点值<attr name="fromAlpha" format="float"/>//整型值<attr name="frameDuration" format="integer"/>//字符串<attr name="text" format="string"/>//百分数<attr name="pivotX" format="fraction"/>//枚举值<attr name="orientation"> <enum name="horizontal" value="0"/> <enum name="vertical" value="1"/></attr>//位或运算<attr name="windowSoftInputMode"> <flag name="stateUnspecified" value="0"/> <flag name="stateUnchanged" value="1"/></attr>//多类型<attr name="background" format="reference|color"/></declare-styleable> 

“`

attrs.xml进行属性声明

而以上三个变量的值,就需要用到一个东西:attrs(这个位于values下面,名称不一定是attrs你可以随便取,但是大家都取attrs),来看看这个里面有什么:

declare-styleable的name
就是自定义的名称用于布局文件里去attr的name是属性名称

澳门威斯尼人平台登录 4

<?xml version="1.0" encoding="utf-8"?><resources> <declare-styleable name="zyView"> <attr name="Text" format="string"/> <attr name="textColor" format="color"/> </declare-styleable></resources>

里面有四个值,分别对应标题的名字,图标的资源文件,图标宽高。关于format:

添加到布局文件

“`

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:andro xmlns:zyView="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" > <rcjs.com.customview.ZYView android:layout_width="wrap_content" android:layout_height="wrap_content" zyView:Text="rcjs" /></RelativeLayout>
  1. reference:参考某一资源ID。如:@drawable/ic_laucher

  2. color:颜色值。

  3. boolean:布尔值。

  4. dimension:尺寸值。

  5. float:浮点值。用法:android:fromAlpha =”1.0  “android:toAlpha =”0.7”

  6. integer:整型值。用法:android:frameDuration =”100″

  7. string:字符串。

  8. fraction:百分数。用法:android:pivotX =”200%”

  9. enum:枚举值。

注意事项:**命名空间:
**xmlns:前缀=”

澳门威斯尼人平台登录 5

在构造函数中获取属性值,注意!!!我想有一些人应该会很郁闷
,复制粘贴了自定义view.class后,发现自定义view的构造方法里面获得资源文件里的属性时****,看到R.styleable.XXX这个,然后点击时****找不到具体写的地方。其实这个就在res
-> values ->attrs里。所以要记得去copy哦。

“`

public class ZYView extends View { public ZYView(Context context) { super; } public ZYView(Context context, @Nullable AttributeSet attrs) { super(context, attrs);//获取资源文件里面的属性,由于这里只有一个属性值,不用遍历数组,直接通过R文件拿出color值 //把属性放在资源文件里,方便设置和复用 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.zyView); String text = a.getString(R.styleable.zyView_Text); int textColor = a.getColor(R.styleable.zyView_textColor, Color.WHITE); a.recycle(); }}

定义一个属性可以指定多个类型,比如背景,可以指定资源也可以指定具体的颜色值:

八、结尾这只是让大家知道自定义view的制作需要什么和要哪些步骤。像我这种完全一窍不通的,然后一下子去接触自定义view的,是会很糊涂的,所以,在此,小僧稍微笔记一波,助人助己。而具体的对自定义view的学习,请待续。。。当然,现在已有大佬们写了很多博客。请参考这篇总的去学习:

“`

<attr name =”background”format =”reference|color”/>

定义一个初始化的方法:initLayout(context,attrs);

private void initLayout(Context context,@NullableAttributeSet attrs) {

View.inflate(context,R.layout.base_menu_item_layout,
this);//将布局文件加载进来,第三个参数this指装载这个布局的父容器

imgLeft= (ImageView) findViewById(R.id.img_myzne);

img_tip= (ImageView) findViewById(R.id.img_tip);

tv= (TextView) findViewById(R.id.tv);

TypedArray typedArray =
context.obtainStyledAttributes(attrs,R.styleable.base_menu_item);

String title =
typedArray.getString(R.styleable.base_menu_item_menu_item_name);

drawable=
typedArray.getDrawable(R.styleable.base_menu_item_drawableleft_src);

mWidth=typedArray.getDimensionPixelSize(R.styleable.base_menu_item_drawableleft_width,0);

mHeight=typedArray.getDimensionPixelSize(R.styleable.base_menu_item_drawableleft_height,

0);

//这个必须调用

typedArray.recycle();

//然后将获取到的标题和图标资源设置到控件上

tv.setText(title);

imgLeft.setImageDrawable(drawable);

}

“`

发表评论

电子邮件地址不会被公开。 必填项已用*标注