【Android】圖文解密Android View

1、簡介

首先,簡單介紹一下大家比較熟悉的Android View構成,如下圖所示。

在這裏插入圖片描述

一個App可以有多個Activity。Activity是Android的四大組件之一,四大組件ABCS即A-Activity,B-BroadcastReceiver,C-ContentProvider,S-Service。每個Activity都有一個Window,Window是個abstract類,目前的唯一實現類爲PhoneWindow,PhoneWindow是一種policy,從名字來看,可能還會有WatchWindow、TvWindow、CarWindow等。另外,還有一種特殊的Window,懸浮窗,這裏不作詳細介紹。PhoneWindow中的top-level View是DecorView,DecorView實際上是ViewGroup。DecorView包含DecorCaptureView和ContentRoot,ContentRoot便是各種父子關係的View樹。下圖是相關的class圖。

在這裏插入圖片描述

2、View與ViewGroup

本文主要介紹View及ViewGroup。View是個基本的UI控件,在屏幕上佔據一個矩形區域,負責描畫和處理事件。一些Widget如TextView、ImageView等是View的子類。ViewGroup也是View的子類,ViewGroup作爲View的容器本身不可見,常見的Layout如RelativeLayout、LinearLayout等便是ViewGroup的子類。下圖是相關的class圖。

在這裏插入圖片描述

3、UiThread

View和ViewGroup使用了UiThread註解,表示必須在UI線程即主線程中使用,包括構造函數以及其它所有的函數調用。相關代碼如下。

@UiThread
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {}

@UiThread
public abstract class ViewGroup extends View implements ViewParent, ViewManager {}

4、屬性

Android定義了一些屬性,可以在xml中設置,也可以在java中使用。以TextView Widget的text屬性爲例,如下例子在xml中將text設置爲“Hello World!”,text的xmlns爲andriod,然後在java中使用R.id獲取TextView對象後,通過getText()獲取text屬性,通過setText()設置text屬性爲“xxx”。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>
package com.example.view;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

public class MainActivity extends Activity {
    private static final String TAG = "ViewTest";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView textView = findViewById(R.id.text_view);
        String text = textView.getText().toString();
        Log.d(TAG, "MainActivity onCreate xml text:" + text);
        textView.setText("xxx");
        text = textView.getText().toString();
        Log.d(TAG, "MainActivity onCreate set text:" + text);
    }
}

Log輸出如下。

2019-01-14 14:16:09.856 10278-10278/com.example.view D/ViewTest: MainActivity onCreate xml text:Hello World!
2019-01-14 14:16:09.857 10278-10278/com.example.view D/ViewTest: MainActivity onCreate set text:xxx

下面我們來分析一下xml與java是如何把TextView的text屬性聯繫在一起的。TextView在xml中可以設置哪些屬性,可以在源碼frameworks/base/core/res/res/values/attrs.xml中看到,格式固定,內容如下。

    <declare-styleable name="TextView">
        <!-- Text to display. -->
        <attr name="text" format="string" localization="suggested" />
    </declare-styleable>

在java中,通過getText()得知,text屬性值保存在一個類型爲CharSequence的mText成員變量中,代碼如下所示。

    // Do not update following mText/mSpannable/mPrecomputed except for setTextInternal()
    @ViewDebug.ExportedProperty(category = "text")
    @UnsupportedAppUsage
    private @Nullable CharSequence mText;

    @ViewDebug.CapturedViewProperty
    public CharSequence getText() {
        return mText;
    }

setText()有5個版本,如下所示。

    @android.view.RemotableViewMethod
    public final void setText(CharSequence text) {}
    public void setText(CharSequence text, BufferType type) {}
    public final void setText(char[] text, int start, int len) {}
    @android.view.RemotableViewMethod
    public final void setText(@StringRes int resid) {}
    public final void setText(@StringRes int resid, BufferType type) {}

setText()最終都是通過setTextInternal()實現的,從中可以確定text屬性值確實是保存在了mText成員變量中,代碼如下所示。

    // Update mText and mPrecomputed
    private void setTextInternal(@Nullable CharSequence text) {
        mText = text;
        mSpannable = (text instanceof Spannable) ? (Spannable) text : null;
        mPrecomputed = (text instanceof PrecomputedText) ? (PrecomputedText) text : null;
    }

下面來看一下TextView的構造函數中是如何解析xml中設置的text屬性的。在構造函數中,通過R.styleable.TextView獲取TypedArray類型的AttributeSet,然後通過R.styleable.TextView_text獲取text屬性值,默認爲空字符串,代碼如下所示。

    public TextView(Context context) {
        this(context, null);
    }

    public TextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, com.android.internal.R.attr.textViewStyle);
    }

    public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    @SuppressWarnings("deprecation")
    public TextView(
            Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        setTextInternal("");
        final Resources.Theme theme = context.getTheme();
        TypedArray a = theme.obtainStyledAttributes(attrs,
                com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);
        int n = a.getIndexCount();
        for (int i = 0; i < n; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case com.android.internal.R.styleable.TextView_text:
                    textIsSetFromXml = true;
                    mTextId = a.getResourceId(attr, ResourceId.ID_NULL);
                    text = a.getText(attr);
                    break;
            }
        }
        a.recycle();
        BufferType bufferType = BufferType.EDITABLE;
        // ...
        setText(text, bufferType);
    }

下面是TextView的構造函數中解析text屬性的時序圖。

在這裏插入圖片描述

我們可以根據TextView的text屬性的用法,自定義屬性,format支持integer、string、boolean、float、fraction、enum、color、dimension、flags、reference十種格式。首先在res/values/attrs.xml中定義屬性,代碼如下所示。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomView">
        <attr name="name" format="string" />
        <attr name="number" format="integer" />
    </declare-styleable>
</resources>

同樣在java中解析自定義的屬性,代碼如下所示。

package com.example.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

public class CustomView extends View {
    private static final String TAG = "ViewTest";

    public CustomView(Context context) {
        this(context, null);
    }

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
        String name = array.getString(R.styleable.CustomView_name);
        int number = array.getInt(R.styleable.CustomView_number, 0);
        array.recycle();
        Log.d(TAG, "CustomView name:" + name);
        Log.d(TAG, "CustomView number:" + number);
    }
}

layout中對於CustomView和自定義屬性的用法如下代碼所示。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.example.view.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:name="Tom"
        app:number="18" />

</android.support.constraint.ConstraintLayout>

Log輸出如下。

2019-01-14 14:16:09.844 10278-10278/com.example.view D/ViewTest: CustomView name:Tom
2019-01-14 14:16:09.844 10278-10278/com.example.view D/ViewTest: CustomView number:18

在java中獲取xml中的一個View對象,通過id獲取,id是一個View的身份,用於區分不同的View。除此之外,還有個tag屬性,tag用來記載View的額外信息,使用tag的好處是方便存取,而不用將這些信息放在獨立於View的其它結構中。

5、focus

focus表示UI交互的元素,可以是Activity、Window、View,擁有focus即表示當前元素可以與UI進行交互,如響應input事件。下圖先列出View和ViewGroup中與focus相關的函數,只是列出了函數名和返回值類型,沒有列出具體的參數。

在這裏插入圖片描述

從函數名大致可以明白這個函數的作用。其中,onWindowFocusChanged表示View所在Window的focus發生變化時會調用這個函數,onFocusChanged表示View的focus發生變化時會調用這個函數,還可以通過setOnFocusChangeListener設置View的focus變化的Listener以監聽focus變化。另外,isFocused表示View是否擁有focus,isFocusable表示View是否支持focus。

下面分析View中onWindowFocusChanged的源碼實現。onWindowFocusChanged是dispatchWindowFocusChanged調用的,在onWindowFocusChanged中,主要作了三件事情:一是在Window沒有focus時設置press狀態爲false,這個很容易理解,同時移除LongPress和Tap的Callback,因爲LongPress和Tap屬於Gesture,而Gesture依賴於press狀態;二是設置InputMethodManager對應的focus狀態,設置爲focusIn或focusOut;最後通過refreshDrawableState刷新View狀態。源碼如下所示。

    public void dispatchWindowFocusChanged(boolean hasFocus) {
        onWindowFocusChanged(hasFocus);
    }

    public void onWindowFocusChanged(boolean hasWindowFocus) {
        InputMethodManager imm = InputMethodManager.peekInstance();
        if (!hasWindowFocus) {
            if (isPressed()) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            if (imm != null && (mPrivateFlags & PFLAG_FOCUSED) != 0) {
                imm.focusOut(this);
            }
            removeLongPressCallback();
            removeTapCallback();
            onFocusLost();
        } else if (imm != null && (mPrivateFlags & PFLAG_FOCUSED) != 0) {
            imm.focusIn(this);
        }

        refreshDrawableState();
    }

View中onFocusChanged的源碼實現與onWindowFocusChanged類似,不同的是添加了CallSuper註解,表示View的子類重寫onFocusChanged時需要調用父類即View本身的onFocusChanged另外還通過註冊的監聽focus的Listener通知focus變化。源碼片段如下所示。

    @CallSuper
    protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction,
            @Nullable Rect previouslyFocusedRect) {
        // ...
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnFocusChangeListener != null) {
            li.mOnFocusChangeListener.onFocusChange(this, gainFocus);
        }
        // ...
    }

6、Listener

View中提供了很多Listener,如上面提到的OnFocusChangeListener,用於監聽View變化,這些Listener保存在一個ListenerInfo的class中,如下代碼所示。

    static class ListenerInfo {
        /**
         * Listener used to dispatch focus change events.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        @UnsupportedAppUsage
        protected OnFocusChangeListener mOnFocusChangeListener;

        /**
         * Listeners for layout change events.
         */
        private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;

        protected OnScrollChangeListener mOnScrollChangeListener;

        /**
         * Listeners for attach events.
         */
        private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;

        /**
         * Listener used to dispatch click events.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        @UnsupportedAppUsage
        public OnClickListener mOnClickListener;

        /**
         * Listener used to dispatch long click events.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        @UnsupportedAppUsage
        protected OnLongClickListener mOnLongClickListener;

        /**
         * Listener used to dispatch context click events. This field should be made private, so it
         * is hidden from the SDK.
         * {@hide}
         */
        protected OnContextClickListener mOnContextClickListener;

        /**
         * Listener used to build the context menu.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        @UnsupportedAppUsage
        protected OnCreateContextMenuListener mOnCreateContextMenuListener;

        @UnsupportedAppUsage
        private OnKeyListener mOnKeyListener;

        @UnsupportedAppUsage
        private OnTouchListener mOnTouchListener;

        @UnsupportedAppUsage
        private OnHoverListener mOnHoverListener;

        @UnsupportedAppUsage
        private OnGenericMotionListener mOnGenericMotionListener;

        @UnsupportedAppUsage
        private OnDragListener mOnDragListener;

        private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;

        OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;

        OnCapturedPointerListener mOnCapturedPointerListener;

        private ArrayList<OnUnhandledKeyEventListener> mUnhandledKeyListeners;
    }

	@UnsupportedAppUsage
    ListenerInfo mListenerInfo;

這些Listener一般都有getter/setter函數,如下代碼中的OnFocusChangeListener。

    @UnsupportedAppUsage
    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }

    public OnFocusChangeListener getOnFocusChangeListener() {
        ListenerInfo li = mListenerInfo;
        return li != null ? li.mOnFocusChangeListener : null;
    }

    public void setOnFocusChangeListener(OnFocusChangeListener l) {
        getListenerInfo().mOnFocusChangeListener = l;
    }

Listener中的註解UnsupportedAppUsage表示不公開到SDK,通過SDK無法直接使用這些內容。

7、Visibility

View的Visibility有三種狀態:visible、invisible和gone。其中,visible表示可見,invisible表示不可見,gone也表示不可見,更深層次的意思是invisible雖然不可見但仍佔據layout空間,而gone則是徹底的不可見,不佔據layout空間。也就說是,在某些情況下,gone會導致其它View重新佈局,如LinearLayout。Visibility操作的函數如下代碼所示。

    @ViewDebug.ExportedProperty(mapping = {
        @ViewDebug.IntToString(from = VISIBLE,   to = "VISIBLE"),
        @ViewDebug.IntToString(from = INVISIBLE, to = "INVISIBLE"),
        @ViewDebug.IntToString(from = GONE,      to = "GONE")
    })
    @Visibility
    public int getVisibility() {
        return mViewFlags & VISIBILITY_MASK;
    }

    @RemotableViewMethod
    public void setVisibility(@Visibility int visibility) {
        setFlags(visibility, VISIBILITY_MASK);
    }

在View的屬性或狀態中,並不是將這些屬性或狀態直接保存在某個變量中,而是通過一個Flag記錄,對這個Flag進行位操作,這是一種好的編程思維。

當View的Visibility發生變化時,會調用onVisibilityChanged,如果是View所在Window的Visibility發生變化時會調用onWindowVisibilityChanged。View在Window中,當View添加到Window中時會調用onAttachedToWindow,當View從Window中移除時會調用onDetachedFromWindow。另外,當從xml中加載layout時,加載完成後,會調用onFinishInflate。

8、Geometry

View是個矩形,包括x和y、width和height兩對基本屬性,單位爲pixel,座標原點在屏幕左上角,一般是相對座標,即子View相對於父View的位置。View的這兩對Geometry屬性又與其它的屬性相關,下面逐個介紹。

在這裏插入圖片描述

  • left:Child View左邊距離Parent View左邊的大小。

  • right:Child View右邊距離Parent View左邊的大小。

  • top:Child View上邊距離Parent View上邊的大小。

  • bottom:Child View下邊距離Parent View上邊的大小。

  • elevation:Child View在Z軸方向上相對於Parent View的深度。

  • translationX:View移動時相對於left的位置。

  • translationY:View移動時相對於top的位置。

  • translationZ:View移動時相對於elevation的位置

  • x:View在x軸方向上的實際位置,x = left + translationX。

  • y:View在y軸方向上的實際位置,y = top + translationY。

  • z:View在z軸方向上的實際位置,z = elevation + translationZ。

  • width:View的實際寬度,width = right - left。

  • height:View的實際高度,height = bottom - top。

  • measuredWidth:measure階段的測量值,與width可能不同。

  • measuredHeight:measure階段的測量值,與height可能不同。

  • paddingLeft:Child View與Parent View在左邊保留的最小間歇。

  • paddingRight:Child View與Parent View在右邊保留的最小間歇。

  • paddingTop:Child View與Parent View在上邊保留的最小間歇。

  • paddingBottom:Child View與Parent View在下邊保留的最小間歇。

  • paddingBegin:從左到右佈局時,等於paddingLeft,否則等於paddingRight。

  • paddingEnd:同paddingBegin相反。

另外,ViewGoup中添加了margin屬性,類似於padding屬性,不同的是,padding屬性影響的是Child,margin屬性影響的是自身。

上面的位置屬性都是相對的,有對應的getter/setter函數,如果獲取絕對座標即相對於屏幕原點的大小,可通過下面的三個函數獲取。

  • getLocationInWindow
  • getLocationOnScreen
  • getGlobalVisibleRect

另外,對於MotionEvent來說,getX、getY返回相對座標,getRawX、getRawY返回絕對座標。

9、Measure、Layout、Draw

Android View的三大流程是measure、layout和draw,對應的都有個Callback即onMeasure、onLayout和onDraw,下圖先從時序上大致瞭解下這三大流程。

在這裏插入圖片描述

首先來看看measure。measure即測量,目的是測量一個View應該有多大,子View的大小受父View大小的影響,這個影響通過MeasureSpec指定。MeasureSpec有三種模式,其中UNSPECIFIED表示父View對子View沒有任何約束,EXACTLY表示父View限制了子View的大小爲具體的值,如xml文件中指定的具體的值或match_parent,AT_MOST表示父View指定了子View的最大值,如xml文件中指定的wrap_content。ViewGroup在測量時,通過measureChildren循環遍歷所有的子View,當子View的狀態不爲GONE時,通過measureChild測量這個子View的大小,這時需要通過getChildMeasureSpec獲得MeasureSpec,然後調用子View的measure進行測量。View的measure,主要的工作就是調用了onMeasure,onMeasure是個真正測量的地方,我們可以Override這個onMeasure做我們想做的事情,最後測量結果保存在了mMeasuredWidth和mMeasuredHeight。

layout即佈局,在measure階段之後,用於指定View的位置和大小。layout過程中,具體代碼實現由setFrame完成,當layout變化時,會調用onSizeChanged以及onLayout和相關的監聽Layout變化的Listener。我們可以Override onLayout,如RelativeLayout的onLayout,它會循環遍歷所有的子View,調用子View的layout函數。

draw即描畫,在ViewGroup中由dispatchDraw發起,然後通過drawChild調到View的draw函數,一個View如何渲染在draw函數指定,依賴於View所屬的layer類型和是否硬件加速。draw包括如下七個步驟。

  1. 描畫背景,drawBackground。
  2. 如果必要,保存canvas的layout用以fading繪製。
  3. 描畫View內容,onDraw。
  4. 描畫孩子,dispatchDraw。
  5. 如果必要,繪製fading並恢復之前保存的layer。
  6. 描畫裝飾,如scrollbar,onDrawForeground。
  7. 描畫默認的高亮focus,drawDefaultFocusHighlight。

10、Event

接收事件最簡單的方法是通過形如setOnXxxListener的函數設置對應事件的監聽者,常用的有如下幾個函數。

  • setOnTouchListener:Touch事件,最基本的事件,包括down、up、move、cancel等。
  • setOnClickListener:Click事件,即快速touch down、up。
  • setOnLongClickListener:Long Click事件,即touch down、keep touch、up。
  • setOnDragListener:Drag事件,即touch down、move。
  • setOnKeyListener:Key事件。

此外,還有如下幾個設置事件監聽者的函數。

  • setOnGenericMotionListener
  • setOnContextClickListener
  • setOnHoverListener
  • setOnCapturedPointerListener

當我們自定義View時,可以Override形如OnXxxEvent的函數,也可以接收對應的事件,有如下幾個函數。

  • onTouchEvent
  • onDragEvent
  • onKeyDown
  • onKeyUp
  • onKeyLongPress
  • onKeyMultiple
  • onKeyPreIme
  • onKeyShortcut
  • onTrackballEvent
  • onGenericMotionEvent
  • onHoverEvent
  • onCancelPendingInputEvents
  • onFilterTouchEventForSecurity
  • onCapturedPointerEvent

在ViewGroup中,有如下幾個Event相關的函數。

  • onInterceptTouchEvent:攔截Touch事件。
  • onInterceptHoverEvent:攔截Hover事件。
  • setMotionEventSplittingEnabled:設置事件分發策略,是否同時派發到多個子View。

事件如何派發?事件派發的相關函數爲dispatchXxxEvent,以dispatchTouchEvent爲例,下面的類圖列出了相關的函數。

在這裏插入圖片描述

ViewGroup的dispatchTouchEvent用於事件分發,決定把事件分發給哪個View或ViewGroup,onInterceptTouchEvent表示是否攔截事件,如果攔截事件,事件將不再往ViewGroup的孩子分發。View的dispatchTouchEvent用於事件執行,首先派發給Listener,沒有Listener或Listener不消費時,將事件發送到onTouchEvent。所有函數都有個boolean類型的返回值,返回true表示消費事件,返回false表示不消費事件。

下面分析事件分發的流程。

  1. dispatchTouchEvent是從ViewRootImpl的InputStage的ViewPostIme階段調過來的。
    直接調用的是dispatchPointerEvent,然後根據Event類型決定調用dispatchTouchEvent還是dispatchGenericMotionEvent。源碼如下所示。
	@UnsupportedAppUsage
    public final boolean dispatchPointerEvent(MotionEvent event) {
        if (event.isTouchEvent()) {
            return dispatchTouchEvent(event);
        } else {
            return dispatchGenericMotionEvent(event);
        }
    }
  1. 接着是ViewGroup的dispatchTouchEvent。在dispatchTouchEvent的開始,首先會通過InputEventConsistencyVerifier進行事件一致性驗證,函數結束時如果事件還沒有被消費,也會通知InputEventConsistencyVerifier,代碼如下所示。
	public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }

        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }

        boolean handled = false;
        // ...
        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }
  1. 接着,所有的代碼處理都在onFilterTouchEventForSecurity返回true的情況下執行,這是一種安全策略,當Window處於Obscured狀態時可以屏蔽事件,代碼如下。
	/**
     * Filter the touch event to apply security policies.
     *
     * @param event The motion event to be filtered.
     * @return True if the event should be dispatched, false if the event should be dropped.
     *
     * @see #getFilterTouchesWhenObscured
     */
    public boolean onFilterTouchEventForSecurity(MotionEvent event) {
        //noinspection RedundantIfStatement
        if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
                && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
            // Window is obscured, drop this touch.
            return false;
        }
        return true;
    }
  1. 接着,對touch down事件進行特殊處理,因爲down是一切事件的開始,所以down事件到來時,要清空原有的狀態,代碼如下。

    	// Handle an initial down.
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Throw away all previous state when starting a new touch gesture.
            // The framework may have dropped the up or cancel event for the previous gesture
            // due to an app switch, ANR, or some other state change.
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }
    
  2. 接着,判斷是否攔截事件,當事件爲touch down或者有FirstTouchTarget時,在允許攔截事件的情況下就會調用onInterceptTouchEvent,進而判斷是否攔截了事件;當事件非touch down且有FirstTouchDown時,必然要攔截這個事件,因爲事件要發給touch down事件的接收者。然後,判斷事件是否cancel,是否可以分發事件到各個孩子。代碼如下所示。

			// Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

            // If intercepted, start normal event dispatch. Also if there is already
            // a view that is handling the gesture, do normal event dispatch.
            if (intercepted || mFirstTouchTarget != null) {
                ev.setTargetAccessibilityFocus(false);
            }

            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
  1. 接着是事件真正分發的邏輯,前提是事件沒有cancel也沒有被攔截。如果事件被攔截,則通過super.dispatchTouchEvent進行處理。可以分發的事件有三種類型,一是touch down事件,二是hover事件,三是多個手指操作且允許事件分發到不同孩子的情況。事件分發時,循環遍歷所有的孩子,通過private函數dispatchTransformedTouchEvent真正實現事件分發。在dispatchTransformedTouchEvent中,其實就是調用了super.dispatchTouchEvent或child.dispatchTouchEvent。前面分析了ViewGroup的dispatchTouchEvent,下面分析View的dispatchTouchEvent。

  2. View的dispatchTouchEvent中,其實就是把事件發送給Listener或onTouchEvent,代碼如下。

		if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
  1. Listener是使用者自己註冊實現的,下面看看View的onTouchEvent做了哪些事情。如果View註冊了Touch Delegate,事件首先會發給Delegate,代碼如下所示。
	public boolean onTouchEvent(MotionEvent event) {
        // ...
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
        // ...
    }

	/**
     * Sets the TouchDelegate for this View.
     */
    public void setTouchDelegate(TouchDelegate delegate) {
        mTouchDelegate = delegate;
    }

然後,是對touch down、up、move、cancel事件的處理。至此,事件處理結束。下面是事件分發的流程圖。

在這裏插入圖片描述

下面再簡單介紹下Android中相關的Gesture。Gesture有兩種用法,一種是GestureDector,用以識別MotionEvent爲哪些Gesture,支持的Gesture如下。

public class GestureDetector {
    public interface OnGestureListener {
        boolean onDown(MotionEvent e);
        void onShowPress(MotionEvent e);
        boolean onSingleTapUp(MotionEvent e);
        boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
        void onLongPress(MotionEvent e);
        boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
    }
    public interface OnDoubleTapListener {
        boolean onSingleTapConfirmed(MotionEvent e);
        boolean onDoubleTap(MotionEvent e);
        boolean onDoubleTapEvent(MotionEvent e);
    }
    public interface OnContextClickListener {
        boolean onContextClick(MotionEvent e);
    }
}

另一種Gesture是通過GestureLibrary識別的觸摸屏上的軌跡線,詳見android.gesture.Gesture。

11、Scroll

View的Scroll表示可以移動View,準確的來說是移動View中的內容。例如,向右移動View 100個pixel,相當於向左移動View的內容100個pixel。下面列出View類中與Scroll相關主要的幾個變量和函數。

在這裏插入圖片描述

Scroll的x和y分別保存在變量mScrollX和mScrollY中。setScrollX、setScrollY和scrollBy都通過scrollTo實現,源碼如下所示。

	public void setScrollX(int value) {
        scrollTo(value, mScrollY);
    }

	public void setScrollY(int value) {
        scrollTo(mScrollX, value);
    }

	public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

當View被Scroll時,會調用onScrollChanged函數,如果通過setOnScrollChangeListener設置監聽Scroll變化的Listener,還會通知這個Listener,通知是在onScrollChanged函數中發出的,而onScrollChanged函數是在scrollTo中調用的,下面是scrollTo的源碼實現。

	public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

View在Scroll時,實際上是作了一個動畫。真正的動作發生在postInvalidateOnAnimation,然後通過ViewRootImpl、 Choreographer、DisplayEventReceiver、native完成,時序圖如下。

在這裏插入圖片描述

12、Animation

View提供了播放動畫的接口,下面列出函數名。

  • startAnimation:播放動畫。
  • setAnimation:設置動畫。
  • clearAnimation:取消動畫。
  • getAnimation:獲取動畫。
  • postInvaidateOnAnimation:用於Display下一幀時的動畫。
  • postOnAnimation:指定下一個動畫幀時需要做的事情。
  • postOnAnimationDelayed:指定下一個動畫幀時需要做的事情,有延遲。
  • onAnimationStart:動畫開始時調用。
  • onAnimationEnd:動畫結束時調用。

下面是Animation相關的類圖。

在這裏插入圖片描述

可以看出Animation支持的動畫種類有限,包括alpha、平移、縮放和旋轉,以及組合動畫AnimationSet,是Android早期的產品,後期又退出了更強大的Animator動畫框架。兩者相比,從版本兼容性來說,Animation更好;從效率來說,Animator使用了反射,效率稍差;從應用角度來說,Animator可以用於任意對象,應用範圍更廣;從動畫效果來看,Animator會更新View的實際位置,而Animation只是提供了一種動畫效果,實際位置保持不變。

下面是Animator相關的類圖。

在這裏插入圖片描述

13、佈局

Android有各種各樣的佈局,這些佈局都繼承自ViewGroup類,下面是幾種主要佈局的類圖。

在這裏插入圖片描述

  • LinearLayout:線性佈局,包括水平方向和垂直方向。
  • GridLayout:網格佈局。
  • AbsoluteLayout:絕對佈局。
  • RelativeLayout:相對佈局。
  • FrameLayout:幀佈局,在z軸方向上的佈局,上面的會覆蓋下面的。

上面的這些佈局都繼承自ViewGroup,必定會包含一些Child,對這些Child的管理通過ViewManager和ViewParent完成,文章開頭列出了相關的類圖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章