Android面试准备:自定义控件(二)

LayoutInflater介绍

  1. LayoutInflater的作用
    Android中使用LayoutInflater类来加载布局(在Activity中常见的加载布局方法setContentView()它的底层就是使用了LayoutInflater来加载布局的。)
  2. LayoutInflater的基本用法
    1、获取到LayoutInflater的实例(有两种方式获得LayoutInflater实例的方法)
//第一种
LayoutInflater layoutInflater = LayoutInflater.from(context);
//第二种(第一种方式其实是对第二种方式的封装)
LayoutInflater layoutInflater = (LayoutInflater) context       .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 

2、通过LayoutInflater的实例调用inflate()方法来加载布局了

layoutInflater.inflate(resourceId, root); 

inflate()方法一般接收两个参数,第一个参数就是要加载的布局id,第二个参数是指给该布局的外部再嵌套一层父布局,如果不需要就直接传null,返回值为加载布局的View对象。

3、例如,将一个Button加载到LinearLayout布局中:

public class MainActivity extends Activity {    
    private LinearLayout mainLayout;  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        mainLayout = (LinearLayout) findViewById(R.id.main_layout);  
        LayoutInflater layoutInflater = LayoutInflater.from(this);  
        View buttonLayout = layoutInflater.inflate(R.layout.button_layout, null);  
        mainLayout.addView(buttonLayout);  
    }  
}  

4、LayoutInflater广泛应用于需要动态加载布局的地方,譬如说我们创建的为ListView中每一个Item加载布局的时候 + 我们平时使用的几种常用的布局其实它们都是有父布局的,父布局的名称是FrameLayout,这就是为什么我们平时简单的布局会有一行标题栏。

视图View的绘制流程

任何一个视图要显示在屏幕上,都要经过相应的绘制流程后才能显示出来。视图的绘制流程主要涉及到的方法有:measure方法,用于测量视图的大小,也就是说父视图会根据onMeasure这个方法来测量(计算)子视图的大小;还有就是layout方法,用于给视图布局的,也就是确定视图在父视图中的位置;最后的话会调用draw方法绘制视图。
1. onMeasure():就是用于测量视图的大小,也就是说父视图会根据onMeasure这个方法来测量(计算)子视图的大小。
1、View的绘制流程首先会调用measure()方法。measure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度、高度的规格(specMode)和大小(pecSize)
2、,widthMeasureSpec和heightMeasureSpec这两个值又是从哪里得到的呢?通常情况下,这两个值都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小。
3、measure()方法:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  
    if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||  
            widthMeasureSpec != mOldWidthMeasureSpec ||  
            heightMeasureSpec != mOldHeightMeasureSpec) {  
        mPrivateFlags &= ~MEASURED_DIMENSION_SET;  
        if (ViewDebug.TRACE_HIERARCHY) {  
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);  
        }  
        onMeasure(widthMeasureSpec, heightMeasureSpec);  
        if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {  
            throw new IllegalStateException("onMeasure() did not set the"  
                    + " measured dimension by calling"  
                    + " setMeasuredDimension()");  
        }  
        mPrivateFlags |= LAYOUT_REQUIRED;  
    }  
    mOldWidthMeasureSpec = widthMeasureSpec;  
    mOldHeightMeasureSpec = heightMeasureSpec;  

①、measure()这个方法是final的,因此我们无法在子类中去重写这个方法,说明Android是不允许我们改变View的measure框架的。然后在第9行调用了onMeasure()方法,这里才是真正去测量并设置View大小的地方,默认会调用getDefaultSize()方法来获取视图的大小。
②、当然,一个界面的展示可能会涉及到很多次的measure,因为一个布局中一般都会包含多个子视图,每个视图都需要经历一次measure过程。
③、onMeasure()方法是可以重写的,也就是说,如果你不想使用系统默认的测量方式,可以按照自己的意愿进行定制
④、视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,而开发人员可以在XML文件中指定视图的大小,然后视图本身会对最终的大小进行排版;
2. onLayout()方法:用于给视图布局的,也就是确定视图的位置。
1、ViewRoot再测量好视图大小之后,接着会调用View的layout()方法来指定视图的位置
2、layout根据一个初始坐标点 + measure方法中测量的视图的高度和宽度来确定视图在父视图中的的位置

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight); 

3、View中的onLayout()方法就是一个空方法,因为onLayout()过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置
4、ViewGroup中的onLayout()方法竟然是一个抽象方法,这就意味着所有ViewGroup的子类都必须重写这个方法。像LinearLayout、RelativeLayout等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。
3. onDraw:在这里才真正地开始对视图进行绘制
1、measure和layout的过程都结束后,接下来就进入到draw的过程了。
2、ViewRoot中的代码会继续执行并创建出一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工作。draw()方法内部的绘制过程总共可以分为六步,其中第二步和第五步在一般情况下很少用到,因此这里我们只分析简化后的绘制过程。

public void draw(Canvas canvas) {  
    if (ViewDebug.TRACE_HIERARCHY) {  
        ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);  
    }  
    final int privateFlags = mPrivateFlags;  
    final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&  
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);  
    mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;  
    // Step 1, draw the background, if needed  
    int saveCount;  
    if (!dirtyOpaque) {  
        final Drawable background = mBGDrawable;  
        if (background != null) {  
            final int scrollX = mScrollX;  
            final int scrollY = mScrollY;  
            if (mBackgroundSizeChanged) {  
                background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);  
                mBackgroundSizeChanged = false;  
            }  
            if ((scrollX | scrollY) == 0) {  
                background.draw(canvas);  
            } else {  
                canvas.translate(scrollX, scrollY);  
                background.draw(canvas);  
                canvas.translate(-scrollX, -scrollY);  
            }  
        }  
    }  
    final int viewFlags = mViewFlags;  
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;  
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;  
    if (!verticalEdges && !horizontalEdges) {  
        // Step 3, draw the content  
        if (!dirtyOpaque) onDraw(canvas);  
        // Step 4, draw the children  
        dispatchDraw(canvas);  
        // Step 6, draw decorations (scrollbars)  
        onDrawScrollBars(canvas);  
        // we're done...  
        return;  
    }  
}  

具体每一步执行的流程为:
①、绘制视图背景
③、对视图的内容进行绘制。可以看到,这里去调用了一下onDraw()方法,这也是一个空方法,在自定义控件的时候可能要重写这个方法
④、绘制子视图
⑥、绘制滚动条(视图都有这个滚动条,只是没有显示而已)
3、onDraw方法主要是依靠canvas、paint对象来绘制View。

视图状态及重绘流程分析

  1. 视图状态
    根据视图的不同状态定制不同的效果。譬如我们常见的一个按钮点击之后,有点击的效果,这就是因为根据不同的视图状态设置不同的背景图片。
    1、几种常见的视图状态
    ①、enabled状态:当前视图是否可用,可用就代表能响应onTouch事件。
    ②、 focused状态:表示当前视图是否获得到焦点
    ③、 selected状态:表示当前视图是否处于选中状态
    ④、 pressed状态:表示当前视图是否处于按下状态
  2. 我们可以在项目的drawable目录下创建一个selector文件:compose_bg.xml,在这里配置每种状态下视图对应的背景图片。
<selector xmlns:android="http://schemas.android.com/apk/res/android">  
    <item android:drawable="@drawable/compose_pressed" android:state_pressed="true"></item>  
    <item android:drawable="@drawable/compose_pressed" android:state_focused="true"></item>  
    <item android:drawable="@drawable/compose_normal"></item>  
</selector>  

1、当视图处于正常状态的时候就显示compose_normal这张背景图,当视图获得到焦点或者被按下的时候就显示compose_pressed这张背景图。
2、在布局文件中使用

    <Button   
        android:id="@+id/compose"  
        android:layout_width="60dp"  
        android:layout_height="40dp"  
        android:layout_gravity="center_horizontal"  
        android:background="@drawable/compose_bg"  
        />  
  1. 底层实现步骤:
protected void drawableStateChanged() {  
    //获得selector对象
    Drawable d = mBGDrawable;  
    if (d != null && d.isStateful()) { 
    //获取视图状态 + 根据不同的状态更新视图
        d.setState(getDrawableState());  
    }  
}

1、获得selector对象
2、获取视图状态
3、根据不同的状态更新视图

视图重绘

假如要求动态更新视图,譬如说之前视图状态的改变来显示不同的背景这就涉及到视图重绘操作。要对视图进行重绘,可以调用invalidate()方法来实现。
1、根据视图的状态来改变视图其实底层就是调用了invalidate()来实现视图重绘的。
2、invalidate()方法虽然会调用到performTraversals()方法,也就是一个视图的绘制流程方法,不过它底层不会再调用measure方法和layout方法了,而只是调用draw方法来重新绘制视图。

自定义View的三种实现方式

  1. 继承View类,重写onDraw方法,调用invalidate方法重新绘制View(譬如说计数器)
  2. 组合控件,即将几种控件组合起来形成一个新的控件,这个新的组合控件就会整合了原来每一个控件的功能(譬如说新浪微博中ListView第一行上面的状态栏)
  3. 继承某一个控件,在该控件的基础之上添加新的功能。(譬如说在ListView中每一个Item设置滑动监听,显示删除按钮)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值