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, 这几个细节我们要处理,代码: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 为例吧: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 上。填充的布局如下:<?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布局的使用时, 可以直接在布局中通过自定义属性设置数据:<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" />也可以通过暴露的方法设置数据至此,自定义控件四种继承方式讲解完毕, 下面看一三个自定义控件的效果
新闻热点
疑难解答