《Android群英传》学习笔记之Android控件架构与自定义控件详解

一、Android控件架构:

  • 控件大致分为两类:ViewGroup控件与View控件。View是绘制在屏幕上的用户能与之交互的一个对象。而ViewGroup则是一个用于存放其他View(和ViewGroup)对象的布局容器。其中,View是所有UI组件的基类,而 ViewGroup是容纳这些组件的容器,其本身也是从View派生出来的

    • (1)View:View对象是Android平台中用户界面体现的基础单位,它是用来创建交互性的UI组件(如:按钮文本框等等)的widgets的父类。它们提供了诸如文本输入框和按钮之类的UI对象的完整实。
    • (2)ViewGroup:ViewGroup继承自View,是一种特殊的View,它可以装其他的Views(或其他的ViewGroup)。ViewGroup是布局(layouts)和views containers的父类。它的直接子类有: FrameLayout、GridLayout、LinearLayout等等。
    • 下面列出View及ViewGroup的所有子类
      • View派生出的直接子类有
        ImageView,ProgressBar,TextView,ViewGroup,AnalogClock,KeyboardView,ViewStub,SurfaceView;
      • View派生出的间接子类有
        Button,CheckBox,AbsoluteLayout,AdapterView,AdapterViewAnimator,AdapterViewFlipper,AppWidgetHostView,AutoCompleteTextView,CalendarView,CheckedTextView,Chronometer,AbsListView,AbsSeekBar,AbsSpinner,CompoundButton;
      • ViewGroup派生出的直接子类有
        AbsoluteLayout,FrameLayout,LinearLayout,RelativeLayout,AdapterView,FragmentBreadCrumbs,SlidingDrawer;
      • ViewGroup派生出的间接子类有
        ListView,GridView,AbsListView,AbsSpinner,AdapterViewAnimator,AdapterViewFlipper,AppWidgetHostView,CalendarView,DatePicker,DialerFilter,ExpandableListView,Gallery,GestureOverlayView,HorizontalScrollView,ImageSwitcher;
  • View树结构 (eg:通常在Activty中使用的findViewById()方法就是在控件树中以树的深度优先遍历来查找对应元素)
    《Android群英传》学习笔记之Android控件架构与自定义控件详解

  • UI界面架构图(DecorView被设置为整个应用窗口的根View)
    《Android群英传》学习笔记之Android控件架构与自定义控件详解

  • 标准视图树
    《Android群英传》学习笔记之Android控件架构与自定义控件详解

  • 在代码中,当程序在onCreate()方法中调用setContentView()方法后,ActivityManagerService会回调OnResume()方法,此时系统会把整个DecorView添加到PhoneWindow中,并让其显示出来

二、View/ViewGroup的测量与绘制

1、View的测量

  • onMeasure() 方法中测量,借助MeasureSpec类来帮助测量View
  • 测量模式:
    • EXACTLY(精确值模式):指定数值(在onMeasure()方法中,如果不重写方法,只能使用这种测量模式)
    • AT_MOST(最大值模式):例如wrap_content,只要不超过父控件允许的最大尺寸即可
    • UNSPECIFIED(不指定大小):通常在绘制自定义View时使用
  • 当View需要使用wrap_content属性时,重写onMeasure()方法,最终要做的工作就是把测量后的宽高作为参数设置给setMeasuredDimension(宽,高) 方法
  • 自定义测量值
    • 1.从MeasureSpec对象中提取出具体的测量模式和大小
      int specMode = MeasureSpec.getMode(measureSpec)
      int specSize = MeasureSpec.getSize(measureSpec)
      
    • 2.通过测量的模式给出不同的测量值

2、View的绘制

  • 系统2D绘图API需使用Canvas对象绘制,Canvas像画板,使用Paint就可以在上面作画。
  • 通常需要继承View并重写的它的onDraw() 方法进行绘图
  • 要在创建Canvas对象的时候同时传入一个bitmap对象。因为传进去的bitmap与通过这个bitmap创建的Canvas画布是紧紧联系在一起的,这个过程称之为装载画布。这个bitmap用来存储所有绘制在Canvas上的像素信息。
        Canvas canvas = new Canvas(bitmap);
        
        //如果在onDraw()方法中,可将bitmap1再装载到其他Canvas对象中,刷新后会重新装载画布
        canvas.drawBitmap(bitmap1,0,0,null);
        
    

3、ViewGroup的测量:

  • ViewGroup在测量时通过遍历所有子View,从而调用子View的Measure方法来获得每一个子View的测量结果,前面所说的对View的测量,就是在这里进行的。

4、ViewGroup的绘制:

  • ViewGroup如果不是被指定了背景色,通常不需要绘制。但是ViewGroup会使用dispatchDraw() 方法遍历所有的子View,并通过调用子View来完成绘制工作。

三、自定义View

通常情况下,有以下三种方法来实现自定义控件

  • 对现有控件进行拓展
  • 通过组合来实现新的控件
  • 重写View来实现全新的控件

1、对现有控件进行拓展

  • 一般来说,在onDraw() 方法中进行对原生控件的拓展
    @Override
    protected void onDraw(Canvas canvas) {
        //在回调父类方法前,实现自己的逻辑,对TextView来说,即是在绘制文本内容前
        super.onDraw();
        //在回调父类方法后,实现自己的逻辑,对TextView来说,即是在绘制文本内容后
    }
    
    • 程序调用super.onDraw(canvas) 方法来实现原生控件的功能。
    • 通过canvas、paint等对象实现逻辑
  • 一般在构造方法中完成对象的初始化工作,如初始化画笔
    mPaint1 = new Paint();
    mPaint1.setColor(getResources().getColor(android.R.color.holo_blue_light));
    mPaint1.setStyle(Paint.Style.FILL);
    
  • 可通过getPaint() 方法获取到当前绘制原生控件的Paint对象,然后对这个对象设置一些原生控件没有的属性

2、创建复合控件

  • 这种方式通常需要继承一个合适的ViewGroup,再给他添加指定功能的控件,从而组合成新的复合控件

Ⅰ、定义属性

确定属性:在res资源目录的values目录下创建一个attrs.xml的属性定义文件

<resources>
    <declare-styleable name="TopBar">
        <attr name = "title" format = "string" />
        <attr name = "titeTextSize" format = "dimension" />
        <attr name = "titleTextColor" format = "color" />
        <attr name = "rightBackground" format = "reference|color"
        ......
    </declare-styleable>
</resources>
  • 通过 标签声明了使用自定义属性,并通过name属性来确定引用的名称
  • 通过 标签来声明具体的自定义属性,并通过format属性来指定属性的类型
  • 需要注意的是,有的属性可以是颜色属性,也可以是引用属性。比如按钮的背景,可以指定为具体的颜色,也可以把它指定为一张具体的图片,所以使用“|”来分隔不同的属性

获取属性

  • 系统提供了TypedArray这样的数据结构来获取自定义属性集,然后通过TypedArray对象的getString()、getColor() 等方法,就可以获取这些定义的属性值。当获取完所有属性值后,需要调用TypedArray的recycle方法来完成资源的回收。
//通过这个方法,将在attrs.xml中定义的declare-styleable的所有属性的值存储到TypedArray中。
//第一个参数为xml名,第二个参数为style的name
TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.TopBar);

//从TypedArray中取出对应的值来为要设置的属性赋值
mLeftTextColor = ta.getColor(R.styleable.TopBar_leftText,0);
mLeftText = ta.getString(R.styleable.TopBar_leftText);
mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftText);

//获取玩TypedArray的值后,一般要调用recyle的方法来避免重新创建时的错误
ta.recycle();

Ⅱ、组合控件

  • 通过动态添加控件的方式,使用addView() 的方法将重置属性后的系统控件放入自定义的模板中
//为组件元素设置相应的布局元素
mLeftParams = new LayoutParams(LayoutParamas.WRAP_CONTENT, LayoutParamas.MATCH_PARENT);
mLeftParamas.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
//添加到ViewGroup
addView(mLeftButton, mLeftParams);
  • 实现自定义控件的点击事件

定义接口

//接口对象,实现回调机制,在回调方法中,通过映射的接口对象调用接口中的方法,而不用去考虑如何具体实现
public interface topbarClickListener {
    //左按点击事件
    void leftClick();
    //右按点击事件
    void rightClick();
}

暴露接口给调用者(使用者)

//按钮的点击事件,不需要具体的实现
//秩序调用接口的方法,回调的时候,会有具体的实现
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;
}

实现接口回调(定义具体实现者)
在调用者的代码中,调用者需要实现这样一个接口,并完成接口中的方法,确定具体实现逻辑,并使用第二步中暴露的方法,将接口的对象传递进去,从而完成回调。

mTopbar.setOnTopbarClickListener(
        new TopBar.topbarClickListener(){
        
            @Override
            public void rightClick(){
                Toast.makeText(TopBarTest.this, "right", Toast.LENGTH_SHORT).show();
            }
            
            @Override
            public void leftClick(){
                Toast.makeText(TopBarTest.this, "left", Toast.LENGTH_SHORT).show();
            }
            
        }
    )

Ⅲ、引用UI模板

  • 在引用前,需要指定引用第三方控件的命名空间
    //假设将引入的第三方控件的名字空间取名为custom
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    
  • 使用自定义View与系统原生的View 的最大区别就是在申明控件时,需要指定完整的包名,而在引用自定义的属性时,需要使用自定义的xmlns名字。
  • 通过include引用布局文件模板
    <include layout="@layout/topbar" />
    

3、重写View来实现全新的控件

  • 创建一个自定义View,难点在于绘制控件和实现交互
    • 通常需要继承View类,并重写它的onDraw()、onMeasure()等方法来实现绘制逻辑。
    • 同时通过onTouchEvent()等触控事件来实现交互逻辑。

四、自定义ViewGroup

  • ViewGroup存在的目的就是为了对其子View进行管理,为其子View添加显示、响应的规则。
    • 通常需要重写onMeasure() 方法对子View进行测量
    • 重写onLayout() 方法来确定子View的位置
    • 重写onTouchEvent() 方法来增加响应事件

五、事件拦截机制分析

  • Android为触摸事件封装了一个类:MotionEvent
  • 触摸事件先按视图树的顺序从上到下顺序对事件进行分发、拦截,再按逆序进行事件处理或审核。
    • 事件传递的返回值:True,拦截,不继续;false,不拦截,继续流程。
    • 事件处理的返回值:True,处理了,不用审核了;false,给上级处理
  • ViewGroup与View的方法:
    • ViewGroup
      //事件分发
      public boolean dispatchTouchEvent(MotionEvent ev)
      //事件拦截
      public boolean onInterceptTouchEvent(MotionEvent ev)
      //事件审核
      public boolean onTouchEvent(MotionEvent ev)
      
    • View(位于最底层,无事件拦截的方法)
      //事件分发
      public boolean dispatchTouchEvent(MotionEvent ev)
      //事件处理
      public boolean onTouchEvent(MotionEvent ev)