自定义View的分类
继承View
当咱们须要实现的效果是一个不规则效果的时候,那么这时就须要继承 View 来实现了,咱们须要重写 onDraw 方法,在该方法里实现各类不规则的图形和效果。当咱们使用这种方式的时候,须要本身去处理 warp_content 和 padding。html
继承ViewGroup
当系统所提供的 LinearLayout、FrameLayout 等布局控件没法知足咱们的需求时,这时咱们就须要使用这种方式来实现本身想要的布局效果了。当咱们使用这种方式的时候,须要重写 onLayout 方法来对子 View 进行布局,以及测量自己和子 View 宽高,还须要处理自己的 padding 和子 View 的 margin。java
继承已有View
当咱们须要基于已有的 View 进行扩展或修改的时候,那么就可使用这种方式。好比说,咱们须要一个圆角的 ImageView,那么这时就能够继承 ImageView 进行修改了。当咱们使用这种方式的时候,通常不须要本身去处理 wrap_content 和 padding 等,由于系统控件已经帮咱们作好了。android
继承已有布局
这种方式也叫作:自定义组合 View。该方式比较简单,当咱们须要将一组 View 组合在一块儿,方便后期复用的时候,就可使用该方法。当咱们使用这种方式的时候,不须要去处理 ViewGroup 的测量和布局流程,由于系统控件已经帮咱们作好了。git
那么下面咱们就从实例的角度来看看自定义View吧
继承View的实例
当咱们自定义View继承子View时,咱们须要注意的细节有:github
1 View是wrap_content时须要手动测量View的宽高canvas
2 View有padding值的时候须要处理app
在这里咱们写一个规范的自定义View, 画出一个圆ide
注意: 要对 View 的 padding 和 LayoutParams 是 wrap_content 的状况进行处理,不然 padding 将会没法生效、wrap_content 的效果会和 match_parent 同样布局
其中重写onMeasure方法, 判断当是wrap_content的状况时,本身测量view的宽或高this
[java] view plain copy
- package com.example.mycustomviewdemo;
-
- import android.content.Context;
- import android.graphics.Canvas;
- import android.graphics.Color;
- import android.graphics.Paint;
- import android.util.AttributeSet;
- import android.view.View;
-
- /**
- * 继承View的自定义控件
- * 注意 view是wrap_content时须要手动测量View的宽高
- * View有padding值时须要处理
- */
-
- public class MyCircleView extends View {
-
- private Paint mPaint;
- private int mRadius;
-
- public MyCircleView(Context context) {
- this(context,null);
- }
-
- public MyCircleView(Context context, AttributeSet attrs) {
- this(context, attrs,0);
- }
-
- public MyCircleView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- init();
- }
-
- private void init() {
- mPaint = new Paint(); //初始化画笔
- mPaint.setColor(Color.GREEN);
- mPaint.setAntiAlias(true);
-
- mRadius = 80;
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- int widthMode = MeasureSpec.getMode(widthMeasureSpec);
- int heightMode = MeasureSpec.getMode(heightMeasureSpec);
- int widthSize = MeasureSpec.getSize(widthMeasureSpec);
- int heightSize = MeasureSpec.getSize(heightMeasureSpec);
-
- int width = 0;
- int height =0;
- if(widthMode == MeasureSpec.EXACTLY) {
- width = widthSize;
- }else {
- //widthMode == MeasureSpec.AT_MOST模式 本身设置控件宽度
- //当是wrap_content或者给具体dp的时候会走这里
- width = mRadius * 2 + getPaddingRight() + getPaddingLeft();
- }
- if(heightMode == MeasureSpec.EXACTLY) {
- height = heightSize;
- }else {
- height = mRadius * 2 + getPaddingTop() + getPaddingBottom();
- }
- //注意最后 调用这个方法 让属性生效
- setMeasuredDimension(width,height);
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- //处理padding
- int pl = getPaddingLeft();
- int pr = getPaddingRight();
- int pt = getPaddingTop();
- int pb = getPaddingBottom();
-
- int width = getWidth() - pl - pr; //控件自己的宽度
- int height = getHeight() - pt - pb; //控件自己的高度
-
-
- int centerX = width /2 + pl; //中心点的横坐标
- int centerY = height /2 + pt; //中心点的纵坐标
-
-
- canvas.drawCircle(centerX,centerY,mRadius,mPaint);
- }
- }
继承ViewGroup实例
当咱们自定义View继承自ViewGroup的时候,须要实现孩子的onLayout方法指定子View的摆放位置,而且须要重写 onMeasure 方法来测量大小。在这个实例当中,咱们简单模仿下 LinearLayout ,只不过只实现其 Vertical 模式,在这个实例当中,咱们须要注意的细节有:
1 ViewGroup是wrap_content时须要手动测量
2 当ViewGroup自己有padding值的时候须要处理
3 当子View有margin值时须要处理
规范自定义ViewGroup, 这几个细节咱们要处理,代码:[java] view plain copy
- package com.example.mycustomviewdemo;
-
- import android.content.Context;
- import android.util.AttributeSet;
- import android.view.View;
- import android.view.ViewGroup;
-
- /**
- * 继承ViewGroup实例
- *
- * 注意:
- * ViewGroup是wrap_content须要手动测量
- * 当ViewGroup自己有padding值时要处理
- * 当子view有margin值时要处理
- */
-
- public class MySimpleVerticalLayout extends ViewGroup {
- private Context mContext;
-
- public MySimpleVerticalLayout(Context context) {
- this(context,null);
- }
-
- public MySimpleVerticalLayout(Context context, AttributeSet attrs) {
- this(context, attrs,0);
- }
-
- public MySimpleVerticalLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- mContext = context;
-
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- //获取ViewGroup测量模式 大小
- int widthMode = MeasureSpec.getMode(widthMeasureSpec);
- int heightMode = MeasureSpec.getMode(heightMeasureSpec);
- int widthSize = MeasureSpec.getSize(widthMeasureSpec);
- int heightSize = MeasureSpec.getSize(heightMeasureSpec);
-
- //获取ViewGroup的padding(内边距)值
- int pt = getPaddingTop();
- int pb = getPaddingBottom();
- int pl = getPaddingLeft();
- int pr = getPaddingRight();
-
- //先测量孩子, 才能获得孩子具体的宽高; ------->> 这一步很重要
- measureChildren(widthMeasureSpec,heightMeasureSpec);
-
- int width = 0;
- int height = 0;
- int maxWidth = 0;
- if(widthMode == MeasureSpec.AT_MOST) {
- for(int i = 0; i < getChildCount();i++) {
- View childAt = getChildAt(i);
- if(childAt.getVisibility() == GONE) {
- continue;
- }
- //宽度为孩子中 最宽的一个
-
-
- //孩子还有个MarginLayoutParams属性
- MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childAt.getLayoutParams();
- int childWidth = childAt.getMeasuredWidth() + marginLayoutParams.leftMargin + marginLayoutParams.rightMargin;
- maxWidth = maxWidth > childWidth ? maxWidth : childWidth;
- }
- //将遍历后的最宽的宽度加上左右内边距 赋值
- width = maxWidth + pl + pr;
-
- }
- if(heightMode == MeasureSpec.AT_MOST) {
- for(int i = 0; i < getChildCount();i++) {
- View childAt = getChildAt(i);
- if(childAt.getVisibility() == GONE) {
- continue;
- }
- //高度为全部的孩子高度之和加上内边距之和
- MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childAt.getLayoutParams();
- height += childAt.getMeasuredHeight() + marginLayoutParams.topMargin + marginLayoutParams.bottomMargin;
- }
- //最终的高度
- height += (pt + pb);
- }
-
- //作判断, 并将值设置
- setMeasuredDimension(widthMode == MeasureSpec.AT_MOST ? width : widthSize,heightMode == MeasureSpec.AT_MOST ? height : heightSize);
-
- }
-
- /**
- * 对子View进行摆放
- * @param changed
- * @param l
- * @param t
- * @param r
- * @param b
- */
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- //viewGroup的padding值影响孩子的摆放
- int pt = getPaddingTop();
- int pb = getPaddingBottom();
- int pl = getPaddingLeft();
- int pr = getPaddingRight();
-
- int cl = 0;
- int ct = 0;
- int cr = 0;
- int cb = 0;
- int bm = 0; //这个bm很神奇
-
- for(int i =0; i < getChildCount();i++) {
- //判断当子view没有被Gone掉时候
- View childAt = getChildAt(i);
- if(childAt.getVisibility() != GONE) {
- //计算每一个View的位置
- MarginLayoutParams marginLayoutParams= (MarginLayoutParams) childAt.getLayoutParams();
- cl = marginLayoutParams.leftMargin;
- ct += marginLayoutParams.topMargin;
- cr = childAt.getMeasuredWidth() + marginLayoutParams.leftMargin;
- cb += childAt.getMeasuredHeight() + marginLayoutParams.topMargin;
- //对子View进行布局, 注意 必定要调用childAt.layout()方法
- childAt.layout(cl + pl, ct + pt + bm, cr + pr,cb + pb + bm);
- ct += childAt.getMeasuredHeight();
- bm += marginLayoutParams.bottomMargin;
- }
- }
- }
-
-
-
- @Override
- public LayoutParams generateLayoutParams(AttributeSet attrs) {
- return new MarginLayoutParams(mContext, attrs);
- }
- }
继承已有View的实例
继承自系统已有View时,通常是对其原有功能进行扩展或者修改, 好比一个Button 在这里注意监听器的使用
继承已有ViewGroup的实例
这种自定义 View 的实现方式也叫作:“自定义组合控件”,是一种比较简单的自定义 View 方式。使用这种方式时,因为是继承已有的系统控件,因此咱们不需去测量、布局、处理 margin、padding等,由于系统控件自己已经处理好了。
当咱们的项目中有一些布局在不少地方都要用到的话,那么第一时间确定就要想到复用了。复用的话,有人可能会想到使用 include 复用布局,可是若是这样的话,当布局改动性很大时,使用 include 并非很灵活。这时候,就可使用 ”继承已有 ViewGroup“ 这种方式了。
下面一个实例,就拿咱们平时可能常常要写的 Item 为例吧:
[java] view plain copy
- package com.example.mycustomviewdemo;
-
- import android.content.Context;
- import android.content.res.TypedArray;
- import android.util.AttributeSet;
- import android.view.LayoutInflater;
- import android.view.View;
- import android.widget.FrameLayout;
- import android.widget.ImageView;
- import android.widget.TextView;
-
- /**
- * 继承已有的ViewGroup 自定义View的实例,经常使用item布局
- */
-
- public class MyCustomItemLayout extends FrameLayout {
- private Context mContext;
-
- private String mLeftText;
- private int mRightImageResourceId;
- private String mRightText;
- private TextView mTxt_left;
- private TextView mTxt_right;
- private ImageView mImg_right;
-
- public void setLeftText(String leftText) {
- mLeftText = leftText;
- }
-
- public void setRightImageResourceId(int rightImageResourceId) {
- mRightImageResourceId = rightImageResourceId;
- }
-
- public void setRightText(String rightText) {
- mRightText = rightText;
- }
-
- public MyCustomItemLayout(Context context) {
- this(context,null);
- }
-
- public MyCustomItemLayout(Context context, AttributeSet attrs) {
- this(context, attrs,0);
- }
-
- public MyCustomItemLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- this.mContext = context;
-
- //取出自定义属性
- TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyCustomItemLayout);
- mLeftText = typedArray.getString(R.styleable.MyCustomItemLayout_leftText);
- //默认图片为箭头
- mRightImageResourceId = typedArray.getResourceId(R.styleable.MyCustomItemLayout_rightImage, R.drawable.ic_arrow_right);
- mRightText = typedArray.getString(R.styleable.MyCustomItemLayout_rightText);
- typedArray.recycle(); //回收释放资源
-
- initView();
-
- initData();
- }
-
- private void initData() {
- //两种初始化数据的方法, 外界经过set方法进行设置; 布局中直接定义
- mTxt_left.setText(mLeftText);
- mTxt_right.setText(mRightText);
- mImg_right.setImageResource(mRightImageResourceId);
- }
-
- private void initView() {
- //注意 这第二个参数传 this; 两个参数的方法默认会调用三个参数的方法, 第二个参数不为null时,至关于三个参数中root不为null,attach为true
- View view = LayoutInflater.from(mContext).inflate(R.layout.layout_customitem, this);
- mTxt_left = (TextView) findViewById(R.id.txt_left);
- mTxt_right = (TextView) findViewById(R.id.txt_right);
- mImg_right = (ImageView) findViewById(R.id.img_right);
- }
-
-
- }
首先自定义一个类,继承自 FrameLayout,固然,这里你也能够选择继承 LinearLayout 或者其余,根据具体需求来。其中在构造中获取了自定义属性,最主要的地方就是填充布局那里,将布局填充到了当前控件也就是自定义的 ViewGroup 上。填充的布局以下:
[html] view plain copy
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="?android:selectableItemBackground"
- android:gravity="center_vertical"
- android:padding="15dp">
-
- <TextView
- android:id="@+id/txt_left"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:drawablePadding="5dp"
- android:ellipsize="end"
- android:maxLines="1"
- android:textColor="@color/text_black"
- android:textSize="@dimen/txt14"/>
-
- <TextView
- android:id="@+id/txt_right"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginLeft="10dp"
- android:layout_weight="1"
- android:ellipsize="end"
- android:gravity="right"
- android:maxLines="1"
- android:textSize="@dimen/txt14"/>
-
- <ImageView
- android:id="@+id/img_right"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginLeft="5dp"
- android:src="@mipmap/ic_arrow_right"/>
- </LinearLayout>
在项目中 有相相似的Item布局的使用时, 能够直接在布局中经过自定义属性设置数据:
[html] view plain copy
- <com.example.mycustomviewdemo.MyCustomItemLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="10dp"
- app:leftText="版本更新"
- app:rightText="V1.1"
- app:rightImage="@drawable/ic_arrow_right"
- />
也能够经过暴露的方法设置数据
至此,自定义控件四种继承方式讲解完毕, 下面看一三个自定义控件的效果