参考
Android自定义控件,你们是如何系统学习的?
Android LayoutInflater原理分析,带你一步步深入了解View(一)
Android视图绘制流程完全解析,带你一步步深入了解View(二)
Android视图状态及重绘流程分析,带你一步步深入了解View(三)
Android自定义View的实现方法,带你一步步深入了解View(四)
一、view的绘制流程
ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联
root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams,panelParentView);
view的绘制流程是从ViewRoot的performTraversals方法开始的,包含以下三个过程:
- 1.measure过程用来测量View宽高
从顶级view开始,在performMeasure调用measure,在measure中调用OnMeasure。如果是一个ViewGroup,onMeasure又会对所有子元素进行measure,完成遍历。
Measure完成之后,就可以通过getMeasuredWidth和getMeasuredHeight方法来获取到View测量后的宽高值。
直接继承View的自定义组件需要重写OnMeasure方法并设置wrap_content时的自身大小。 - 2.layout过程用来确定view在父容器中的位置
performLayout->layout->onLayout,同上遍历所有子元素。
Layout完成以后,可以通过getTop getBottom getLeft getRight来拿到view的四个顶点位置,通过getWidth getHeight来拿到view的最终宽高。 - 3.draw过程用来将View绘制到屏幕上
performDraw->draw->onDraw,同上遍历所有子元素。draw方法完成以后,view内容才会出现在屏幕上。
二、DecorView
DecorView是一个FrameLayout,view层的事件都先经过DecorView,然后才传递给我们的View。
一般情况下,DecorView作为顶级view,内部包含一个竖直方向的LinearLayout。这个LinearLayout包含上下两个部分,分别是标题栏和内容栏。内容栏的id就叫content,经常使用的setContentView就是加到这个内容栏中的,如setContentView(R.layout.hello_world_layout);可以这样获取我们设置的view:
ViewGroup content = (ViewGroup)findViewById(android.R.id.content);
content.getChildAt(0);
三、如何获得某个view的宽高
因为view的measure过程和activity的生命周期不是同步执行的,因此无法保证activity执行onCreate,onStart,onResume时宽高已经测量完毕。如果没有测量完毕,则会返回0.有四种方法可以获得测量后的宽高,这里只介绍一个简单的:
protected void onStart(){
super.onStart();
view.post(new Runnable(){
public void run(){
int width = view.getMeasuredWidth();
int height = view.getMeasureHeight();
}
});
}
通过post可以将一个runnable投递到一个消息队列的尾部,然后等待looper调用此runnable的时候,view也已经初始化好了。
四、自定义view分类
1.继承view重写onDraw方法
这种方式主要用于实现一些不规则效果,即这种效果不方便通过布局组合方式来达到,往往需要静态或者动态显示一些不规则图形。很显然这需要通过绘制的方式来达到,即重写OnDraw方法。采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。
2.继承ViewGroup派生特殊的Layout
这种方式主要用于实现自定义布局,需要合适地处理ViewGroup的测量和布局两个过程,并同时处理子元素的测量和布局过程。
3.继承特定的View
这种方式比较常见,一般是扩展某种已有的View,比如TextView,这种方法比较容易实现,不需要自己支持wrap_content和padding等。
4.继承特定的ViewGroup(比如LinearLayout)
当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法实现。采用这种方法不需要自己处理ViewGroup的测量和布局,需要注意本方法和方法2的区别,一般来说方法2能实现的效果方法4也都能实现,两者的主要差别在于方法2更接近View的底层。
四、自定义View注意事项
1.重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。
2.在draw方法中支持padding和margin
3.尽量不要在View中使用Handler,因为View本身提供了post系列方法,可以替代Handler的作用。
4.view中如果有线程或者动画需要及时停止,否则可能造成内存泄露。当包含view的activity退出或者当前的view被remove时,view的onDetachedFromWindow方法会被调用,相对应的,启动时执行onAttachedToWindow。
5.View带有滑动嵌套时,需要处理好滑动冲突。
五、自定义组件继承自View重写onDraw示例
实现一个具有圆形效果的自定义View,它会在自己的中心点以宽高的最小值为直径绘制一个红色的实心圆。
public class CircleView extends View{
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context){
super(context);
init();
}
public CircleView(Context context,AttributeSet attrs){
super(context,attrs);
init();
}
public CircleView(Context context,AttributeSet attrs,int defStyleAttr){
super(context,attrs,defStyleAttr);
init();
}
private void init(){
mPaint.setColor(mColor);
}
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
//为了让padding生效,要做处理边界
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
int radius = Math.min(width,height)/2;
canvas.drawCircle(paddingLeft+width/2,paddingTop+height/2,radius,mPaint);
}
}
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:orientation="vertical" >
<com.ryg.chapter_4.ui.CircleView
android:id="@+id/circleView1"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="#000000"
android:padding="20dp"
app:circle_color="@color/light_green" />
</LinearLayout>
以下参考Android 深入理解Android中的自定义属性
自定义属性步骤如下:
1.在values目录下面创建自定义属性的xml文件,比如attrs_cirle_view.xml或者attrs.xml文件,名字不限。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color" />
</declare-styleable>
</resources>
在上面的XML中,声明了一个自定义属性集合CircleView,在这个集合里面可以有很多自定义属性,目前只定义了circle_color,格式为颜色。除了颜色格式,还有reference表示资源id,demension表示尺寸,string integer boolean等则表示基本类型。
2.在View的构造方法中解析自定义属性的值并做相应处理。
public class CircleView extends View {
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//加载自定义属性集合CircleView
TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.CircleView);
//解析集合中的circle_color属性,
//它的id是R.styleable.CircleView_circle_color
//格式:styleable name + "_" + attr name
//对应declare-styleable name="CircleView"><attr name="circle_color"...
mColor = a.getColor(R.styleable.CircleView_circle_color,
Color.RED);//Color.RED作为缺省默认值
a.recycle();//释放资源
init();
}
参考解析:TypedArray 为什么需要调用recycle()
3.在布局文件中使用自定义属性
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:orientation="vertical" >
<com.ryg.chapter_4.ui.CircleView
android:id="@+id/circleView1"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="#000000"
android:padding="20dp"
app:circle_color="@color/light_green" />
</LinearLayout>
注意schemas声明xmlns:app="http://schemas.android.com/apk/res-auto"
这个app自定义前缀可以换作其他名字,但要保证CircleView中自定义属性的前缀一致。app:circle_color="@color/light_green"
六、组合控件TopBar
左按钮,右按钮,中间文本
package com.imooc.systemwidget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.RelativeLayout;
import android.widget.TextView;
public class TopBar extends RelativeLayout {
// 包含topbar上的元素:左按钮、右按钮、标题
private Button mLeftButton, mRightButton;
private TextView mTitleView;
// 布局属性,用来控制组件元素在ViewGroup中的位置
private LayoutParams mLeftParams, mTitlepParams, mRightParams;
// 左按钮的属性值,即我们在atts.xml文件中定义的属性
private int mLeftTextColor;
private Drawable mLeftBackground;
private String mLeftText;
// 右按钮的属性值,即我们在atts.xml文件中定义的属性
private int mRightTextColor;
private Drawable mRightBackground;
private String mRightText;
// 标题的属性值,即我们在atts.xml文件中定义的属性
private float mTitleTextSize;
private int mTitleTextColor;
private String mTitle;
// 映射传入的接口对象
private topbarClickListener mListener;
public TopBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public TopBar(Context context) {
super(context);
}
public TopBar(Context context, AttributeSet attrs) {
super(context, attrs);
// 设置topbar的背景
setBackgroundColor(0xFFF59563);
// 通过这个方法,将你在atts.xml中定义的declare-styleable
// 的所有属性的值存储到TypedArray中
TypedArray ta = context.obtainStyledAttributes(attrs,
R.styleable.TopBar);
// 从TypedArray中取出对应的值来为要设置的属性赋值
mLeftTextColor = ta.getColor(
R.styleable.TopBar_leftTextColor, 0);
mLeftBackground = ta.getDrawable(
R.styleable.TopBar_leftBackground);
mLeftText = ta.getString(R.styleable.TopBar_leftText);
mRightTextColor = ta.getColor(
R.styleable.TopBar_rightTextColor, 0);
mRightBackground = ta.getDrawable(
R.styleable.TopBar_rightBackground);
mRightText = ta.getString(R.styleable.TopBar_rightText);
mTitleTextSize = ta.getDimension(
R.styleable.TopBar_titleTextSize, 10);
mTitleTextColor = ta.getColor(
R.styleable.TopBar_titleTextColor, 0);
mTitle = ta.getString(R.styleable.TopBar_title);
// 获取完TypedArray的值后,一般要调用
// recyle方法来避免重新创建的时候的错误
ta.recycle();
mLeftButton = new Button(context);
mRightButton = new Button(context);
mTitleView = new TextView(context);
// 为创建的组件元素赋值
// 值就来源于我们在引用的xml文件中给对应属性的赋值
mLeftButton.setTextColor(mLeftTextColor);
mLeftButton.setBackground(mLeftBackground);
mLeftButton.setText(mLeftText);
mRightButton.setTextColor(mRightTextColor);
mRightButton.setBackground(mRightBackground);
mRightButton.setText(mRightText);
mTitleView.setText(mTitle);
mTitleView.setTextColor(mTitleTextColor);
mTitleView.setTextSize(mTitleTextSize);
mTitleView.setGravity(Gravity.CENTER);
// 为组件元素设置相应的布局元素
mLeftParams = new LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.MATCH_PARENT);
mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
// 添加到ViewGroup
addView(mLeftButton, mLeftParams);
mRightParams = new LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.MATCH_PARENT);
mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);
addView(mRightButton, mRightParams);
mTitlepParams = new LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.MATCH_PARENT);
mTitlepParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
addView(mTitleView, mTitlepParams);
// 按钮的点击事件,不需要具体的实现,
// 只需调用接口的方法,回调的时候,会有具体的实现
mRightButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mListener.rightClick();
}
});
mLeftButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mListener.leftClick();
}
});
}
// 暴露一个方法给调用者来注册接口回调
// 通过接口来获得回调者对接口方法的实现
public void setOnTopbarClickListener(topbarClickListener mListener) {
this.mListener = mListener;
}
/**
* 设置按钮的显示与否 通过id区分按钮,flag区分是否显示
*
* @param id id
* @param flag 是否显示
*/
public void setButtonVisable(int id, boolean flag) {
if (flag) {
if (id == 0) {
mLeftButton.setVisibility(View.VISIBLE);
} else {
mRightButton.setVisibility(View.VISIBLE);
}
} else {
if (id == 0) {
mLeftButton.setVisibility(View.GONE);
} else {
mRightButton.setVisibility(View.GONE);
}
}
}
// 接口对象,实现回调机制,在回调方法中
// 通过映射的接口对象调用接口中的方法
// 而不用去考虑如何实现,具体的实现由调用者去创建
public interface topbarClickListener {
// 左按钮点击事件
void leftClick();
// 右按钮点击事件
void rightClick();
}
}