Android控件架构简述

Android控件架构

Android中的每个控件都会在界面占得一块矩形的区域,控件大致被分为两类:ViewGroup控件与View控件。ViewGroup左为父控件可以包含并管理多个View,让整个界面上的控件形成了一个树形结构(控件树),上层控件负责下层子控件的测量与绘制,并传递交互事件。通常在Activity使用的findViewById()方法,就是在控件树中以树的深度优先遍历来查找对应元素。

树的顶部有一个ViewParent对象,也就是整颗树的控制核心,所有交互管理事件都由它来统一调度和分配,从而控制整个视图(如图1所示)

                              

                                                          图1                                                                                                                                                                       图2

 

在Activity中使用setContentView()设置一个布局,如图2所示,每个Activity都包含一个Window对象,在安卓中Window通常由PhoneWindow来实现,它将一个DecorView设置为整个窗口的根View,而它作为顶层视图,封装了一些窗口操作的通用方法。可以说,DecorView将要显示的具体内容呈现在了PhoneWindow上,所有View的监听事件,都通过WindowManagerService进行接收,并通过Activity对象来回调相应的onClickListener。在显示上,它将屏幕分为两类:TitleView 和ContentView,

 

ContentView:它是一个ID为:content 的  FrameLayout

通过以上过程,我们可以建立起这样的一个标准视图树,如图3:

 

视图树的第二层装载了一个LinearLayout,作为ViewGroup,这一层的布局结构会根据对应的参数设置不同的布局,如最常用的:上面显示TitleBar,下面是Content这样的,而如果我们通过设置requestWindowFeature(Window,FEATURE_NO_TITLE)来设置全屏显示,那布局就只剩下Content了,这就解释了为什么此方法要在 setContentView()之前调用才生效的原因。

图3所绘制的很粗略,当程序在onCreate方法中调用 setContentView 后,aActivityManagerService会回调onResume,此时系统才会把整个DecorView添加到PhoneWindow中,并让其显示,从而最终完成界面的绘制

 

 

View的测量与绘制

View测量

Android在绘制View之前,必须对View进行测量,告诉系统要画一个多大的View,整个过程是在 onMeasure() 方法中进行的

直译为:测量规格,MeasureSpec到底是干什么的?

通过它帮我们测量View,通常在ViewGroup中用到,它可以根据模式调节子View的大小。确切来说,MeasureSpec在很大程度上决定了View的尺寸规格,之所以说很大程度是因为这个过程还受父容器的影响,因为父容器影响View的MeasureSpec的创建过程,在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个MeasureSpec测量View的宽高,注意,这里是测量的宽高,并不一定等于View的最终宽高。

 

MeasureSpec是一个32位的int值,高2位为测量的模式,低30位是测量的大小,使用位运算是为了提高效率

测量的模式可以分为一下三种:

1.        EXACTLY  [ɪg'zæk(t)lɪ]

精确值模式,控件宽或高指定为:“xx dp”或是match_parent时使用

2.        AT_MOST

最大值模式,控件宽或高指定为:wrap_content时使用

3.      UNSPECIFIED  ['spesifaid]

这个属性比较奇怪,它不指定其大小测量模式,View想多大就多大,通常情况下在绘制自定义View时使用

View类默认的onMeasure方法只支持EXACTLY模式,如果想让自定义View支持wrap_content属性,就必须重写该方法来指定wrap_content时的大小,(设置默认大小)

 

有了这些信息,我们就可以控制View最后显示的大小了。

int widthSpec = MeasureSpec.makeMeasureSpec(menuView.getLayoutParams().width, MeasureSpec.EXACTLY);
menuView.measure(widthSpec, heightMeasureSpec);

 

查看onMeasure的源码,发现最终会调用setMeasureDimension方法将测量后的宽高值设置进去。

 

在重写onMeasure中调用了自定义的方法,分别对宽和高进行重新定义,模板代码如下:

 

setMeasuredDimension(
measuredWidth(widthMeasureSpec), measuredHeight(heightMeasureSpec));


private int measuredWidth(int widthMeasureSpec) {
    int result = 0;
    int specMode = MeasureSpec.getMode();
    int specSize = MeasureSpec.getSize();
    if (specMode == MeasureSpec.EXACTLY) {
        //如果为精确值模式,使用指定的size即可
        result = specSize;
    } else {
        result = 200;
        if (specMode = MeasureSpec.AT_MOST) {
        //取出指定的大小与specSize中最小的一个来作为最后的测量值。
            result = Math.min(result, specSize);
        }
    }
    return result;
}

 

 

 

 

不懂得可以看看这个:http://zhujiao.iteye.com/blog/1851689

 

View绘制

当测量好一个View之后,我们就可以简单的重写onDraw方法,并在Canvas上绘制所需要的图形。

Canvas:要想在界面中绘制相应的图像,就必须在Canvas上绘制,它就像一个画板,使用paint就可以作画了,

Canvas canvas=new Canvas( bitmap)

之所以传入一个Bitmap,是因为需要用来存储所有绘制在Canvas上的像素信息的。这个过程称为装载画布。所以当通过这种方式创建的Canvas对象后,调用的所有Canvas.drawXXX方法都发生在这个bitmap上了

ViewGroup的测量与绘制

在前面的分析中说了,ViewGroup会去管理其子View,其中一个管理项目就是负责子View的显示大小,当ViewGroup的大小为wrap_content时,ViewGroup就需要对子View进行遍历,以便获得所有子View的大小,从而决定自己的大小,

         通过遍历子View,并调用measure 方法获得每一个的测量结果

         当测量结束后,就需要将子View放置到合适的位置,这个过程就是View的Layout过程,同样使用遍历来调用子View的Layout方法,指定显示位置,从而决定其布局位置

 

         在自定义ViewGroup时,通常会去重写onLayout方法来控制子View显示的逻辑,同样,如果支持wrap_content属性,那么必须重写onMeasure方法,这点与View是相同的

 

         ViewGroup的绘制

         通常情况下不需要绘制,因为本身没有需要绘制的东西,如果不是指定了背景颜色,那连onDraw方法都不会被调用。但是,ViewGroup会使用dispatchDraw()方法来绘制其子View。

自定义View

在自定义View时,我们通常会重写onDraw来绘制显示内容,如果该View需要使用wrap_content,那么还必须重写onMeasure方法,另外,通过自定义attrs属性,还可以设置新的属性配置值

         在View有一下一些比较重要的回调方法:

l  onFinishInflate() : 从XML加载组件后回调

l  onSizeChanged() : 组件大小改变时回调

l  onMeasure()   : 用于测量

l  onLayout()     : 回调该方法来确定显示的位置

l  onTouchEvent() : 监听到触摸事件时回调

当然,创建自定义View时并不需要重写所有的方法,只需重写特定条件的回调即可

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

l  对现有控件进行拓展

l  通过组合来实现新的控件

l  重写View来实现全新的控件

 

 

 

定义属性:

添加命名空间

xmlns:app="http://schemas.android.com/apk/res/com.example.shang.youkumenu"
<com.example.shang.youkumenu.view.ToggleButton2
    android:id="@+id/tb_switch"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    app:backgroundToggle="@mipmap/switch_background"
    app:slideToggle="@mipmap/slide_button_background"
    app:state="true" />

 

系统提供了TypedArray这样的数据结构来获取自定义属性集,后面引用的styleablede,就是在xml中通过declare-styleable 指定的name名,接下来通过TypedArray对象的getString、getColor等方法,就可以获取值

 

TypedArray typedArray = getContext().obtainStyledAttributes(
        attrs, R.styleable.ToggleButton2, defStyle, 0);
currentState = typedArray.getBoolean(R.styleable.ToggleButton2_state, false);
int backgroundToggle = typedArray.getResourceId(R.styleable.ToggleButton2_backgroundToggle, -1);

 

<resources>
<declare-styleable name="ToggleButton2">
    <attr name="state" format="boolean" />
    <attr name="backgroundToggle" format="reference" />
    <attr name="slideToggle" format="reference" />
</declare-styleable>
</resources>

 

 

 

 

 

触摸事件

Android提供了一整套完善的事件传递,处理机制,来帮助开发者完成准确的事件分配与处理

在MotionEvent里封装了很多属性和方法,比如触摸点的坐标,通过getX方法和getRawX方法获得、通过不同的Action(MotionEvent.ACTION_DOWN….)来进行区分,并实现不同的逻辑。

如此看来,触摸事件还是很简单的,其实就是一个动作类型加坐标点而已,但是我们知道,Android中View的结构是树形的,View和ViewGroup是是嵌套形式存在的,可是触摸事件就一个,那就涉及到该分给谁来使用的问题了。

 

关于事件处理,有以下方法需要知道:

dispatchTouchEvent()

onInterceptTouchEvent()

onTouchEvent()

ViewGroup的级别比较高,比View多了一个方法:onInterceptTouchEvent()

它是事件拦截的核心方法,

事件传递的返回值很好理解true:拦截,不继续; false:不拦截,继续

事件处理的返回值也类似:true:处理了,不用审核了; false:给上级处理

初始情况下返回值都是false

 

MotionEvent:

常见的动作常量:

public staticfinal int ACTION_DOWN             = 0;单点触摸动作

public staticfinal int ACTION_UP               = 1;单点触摸离开动作
public static final int ACTION_MOVE             =2;触摸点移动动作
public static final int ACTION_CANCEL           = 3;触摸动作取消
public static final int ACTION_OUTSIDE          = 4;触摸动作超出边界
public static final int ACTION_POINTER_DOWN     = 5;多点触摸动作
public static final int ACTION_POINTER_UP       = 6;多点离开动作

以下是一些非touch事件
public static final int ACTION_HOVER_MOVE       = 7;
 public static final int ACTION_SCROLL          = 8;
 public static final int ACTION_HOVER_ENTER      = 9;
 public static final int ACTION_HOVER_EXIT       = 10;

 

通过以下常用方法进一步获取触控事件的具体信息:

event.getAction() //获取触控动作比如ACTION_DOWN
         event.getPointerCount(); //获取触控点的数量,比如2则可能是两个手指同时按压屏幕
         event.getPointerId(nID); //对于每个触控的点的细节,我们可以通过一个循环执行    

getPointerId方法获取索引
         event.getX(nID); //获取第nID个触控点的x位置
         event.getY(nID); //获取第nID个点触控的y位置
         event.getPressure(nID); //LCD可以感应出用户的手指压力,当然具体的级别由驱动和物理硬件决定的
         event.getDownTime() //按下开始时间
    event.getEventTime() // 事件结束时间
         event.getEventTime()-event.getDownTime()); //总共按下时花费时间

 

 

ViewDragHelper

基本用法

  1. 在自定义ViewGroup的构造方法里调用ViewDragHelper的静态工厂方法create()创建ViewDragHelper实例

2.      实现ViewDragHelper.Callback
最重要的几个方法是tryCaptureView()、 clampViewPositionVertical()、clampViewPositionHorizontal()、 getViewHorizontalDragRange()、getViewVerticalDragRange()

l  tryCaptureView()里会传递当前触摸区域下的子View实例作为参数,如果需要对当前触摸的子View进行拖拽移动就返回true,否则返回false。

l  clampViewPositionVertical()决定了要拖拽的子View在垂直方向上应该移动到的位置,该方法会传递三个参数:要拖拽 的子View实例、期望的移动后位置子View的top值、移动的距离。返回值为子View在最终位置时的top值,一般直接返回第二个参数即可。

l  clampViewPositionHorizontal()与clampViewPositionVertical()同理,只不过是发生在水平方向上,最终返回的是View的left值。

l  getViewVerticalDragRange()要返回一个大于0的数,才会在在垂直方向上对触摸到的View进行拖动。

l  getViewHorizontalDragRange()与getViewVerticalDragRange()同理,只不过是发生在水平方向上。

  1. 在onInterceptTouchEvent()方法里调用并返回ViewDragHelper的shouldInterceptTouchEvent()方法

 

  1. 在onTouchEvent()方法里调用ViewDragHelper()的processTouchEvent()方法。 ACTION_DOWN事件发生时,如果当前触摸点下要拖动的子View没有消费事件,此时应该在onTouchEvent()返回true,否则将收不 到后续事件,不会产生拖动。

 

5.      上面几个步骤已经实现了子View拖动的效果,如果还想要实现fling效果(滑动时松手后以一定速率继续自动滑动下去并逐渐停止,类似于扔东西)或者松手后自动滑动到指定位置,需要实现自定义ViewGroup的computeScroll()方法,方法实现如下:

@Override
public void computeScroll() {
   
if (mDragHelper.continueSettling(true)) {
       
postInvalidate();
   
}
}

6.       

并在ViewDragHelper.Callback的onViewReleased()方法里调用settleCapturedViewAt()、flingCapturedView(),或在任意地方调用smoothSlideViewTo()方法。

7.      如果要实现边缘拖动的效果,需要调用ViewDragHelper的setEdgeTrackingEnabled()方法,注册想要监听 的边缘。然后实现ViewDragHelper.Callback里的onEdgeDragStarted()方法,在此手动调用 captureChildView()传递要拖动的子View。

 

Callback是理解ViewDragHelper的关键,因为它反映了开始移动到结束的监听过程.

 

ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
    /**
     *  1.返回ture则表示可以捕获该view,你可以根据传入的第一个view参数决定哪些可以捕获
     * @param child 被滑动的子View
     * @param pointerId 多点触摸的手指Id
     * @return true:可以被拖拽
     */
    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        return true;
    }
    /**
     * 2.返回拖拽的范围,返回一个>0的值,决定了动画的执行时长,水平方向是否可以被滑动
     * @param child 子控件
     * @return mRange 范围
     */
    @Override
    public int getViewHorizontalDragRange(View child) {
        return mRange;
    }
    /**
     * 3. 可以在该方法中对child移动的边界进行控制,left , top 分别为即将移动到的位置,比如横向的情况下,我希望只在ViewGroup的内部移动,即:最小>=paddingleft
最大& lt;=ViewGroup.getWidth()-paddingright-child.getWidth
     * @param child 被拖拽的子View
     * @param left 建议移动到的位置
     * @param dx 与旧值的偏移量
     * @return left 移动到的位置
     */
    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        return left;
    }

    /**
     * 4.控件位置变化时调用 :伴随动画、状态更新、事件回调
     * @param changedView 发生改变的子View
     * @param left  水平方向最新的位置
     * @param top   ..
     * @param dx    刚刚发生的偏移量
     * @param dy    ..
     */
    @Override
    public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
        super.onViewPositionChanged(changedView, left, top, dx, dy);
    }

    /**
     * 5.松手之后调用的方法:结束动画
     * @param releasedChild 被释放的子View
     * @param xvel X轴速度
     * @param yvel  Y轴速度
     */
    @Override
    public void onViewReleased(View releasedChild, float xvel, float yvel) {
        super.onViewReleased(releasedChild, xvel, yvel);
};

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值