1. 程式人生 > >安卓自定義View進階-事件分發機制詳解

安卓自定義View進階-事件分發機制詳解

Android 事件分發機制詳解,在上一篇文章 事件分發機制原理 中簡要分析了一下事件分發機制的原理,原理是十分簡單的,一句話就能總結:責任鏈模式,事件層層傳遞,直到被消費。 雖然原理簡單,但是隨著 Android 不斷的發展,實際運用場景也越來越複雜,所以想要徹底玩轉事件分發機制還需要一定技巧,本篇事件分發機制詳解將帶大家瞭解 …

你以為我接下來要講原始碼?
我就不按套路,所有的原始碼都是為了適應具體的應用場景而寫的,只要能夠理解運用場景,理解原始碼也就十分簡單了。所以本篇的核心問題是:正確理解在實際場景中事件分發機制的作用。 會涉及到原始碼,但不是主角。

注意:本文中所有原始碼分析部分均基於 API23(Android 6.0) 版本,由於安卓系統原始碼改變很多,可能與之前版本有所不同,但基本流程都是一致的。

常見事件

既然是事件分發,總要有事件才能分發吧,所以我們先了解一下常見的幾種事件。

根據面向物件思想,事件被封裝成 MotionEvent 物件,由於本篇重點不在於此,所以只會涉及到幾個與手指觸控相關的常見事件:

事件 簡介
ACTION_DOWN 手指 初次接觸到螢幕 時觸發。
ACTION_MOVE 手指 在螢幕上滑動 時觸發,會會多次觸發。
ACTION_UP 手指 離開螢幕 時觸發。
ACTION_CANCEL 事件 被上層攔截 時觸發。

對於單指觸控來說,一次簡單的互動流程是這樣的:

手指落下(ACTION_DOWN) -> 移動(ACTION_MOVE) -> 離開(ACTION_UP)

  • 本次事例中 ACTION_MOVE 有多次觸發。
  • 如果僅僅是單擊(手指按下再擡起),不會觸發 ACTION_MOVE。

事件分發、攔截與消費

關於這一部分內容,上一篇文章 事件分發機制原理 已經將流程整理的比較清楚了,本文會深入細節來研究這些內容。之所以分開講,是為了防止大家被細節所迷惑而忽略了整體邏輯。

表示有該方法。

X 表示沒有該方法。

型別 相關方法 ViewGroup View
事件分發 dispatchTouchEvent
事件攔截 onInterceptTouchEvent X
事件消費 onTouchEvent

View 相關

dispatchTouchEvent 是事件分發機制中的核心,所有的事件排程都歸它管。不過我細看錶格, ViewGroup 有 dispatchTouchEvent 也就算了,畢竟人家有一堆 ChildView 需要管理,但為啥 View 也有?這就引出了我們的第一個疑問。

Q: 為什麼 View 會有 dispatchTouchEvent ?

A: 我們知道 View 可以註冊很多事件監聽器,例如:單擊事件(onClick)、長按事件(onLongClick)、觸控事件(onTouch),並且View自身也有 onTouchEvent 方法,那麼問題來了,這麼多與事件相關的方法應該由誰管理?毋庸置疑就是 dispatchTouchEvent,所以 View 也會有事件分發。

相信看到這裡很多小夥伴會產生第二個疑問,View 有這麼多事件監聽器,到底哪個先執行?

Q: 與 View 事件相關的各個方法呼叫順序是怎樣的?

A: 如果不去看原始碼,想一下讓自己設計會怎樣?

  • 單擊事件(onClickListener) 需要兩個兩個事件(ACTION_DOWN 和 ACTION_UP )才能觸發,如果先分配給onClick判斷,等它判斷完,使用者手指已經離開螢幕,黃花菜都涼了,定然造成 View 無法響應其他事件,應該最後呼叫。(最後)
  • 長按事件(onLongClickListener) 同理,也是需要長時間等待才能出結果,肯定不能排到前面,但因為不需要ACTION_UP,應該排在 onClick 前面。(onLongClickListener > onClickListener)
  • 觸控事件(onTouchListener) 如果使用者註冊了觸控事件,說明使用者要自己處理觸控事件了,這個應該排在最前面。(最前)
  • View自身處理(onTouchEvent) 提供了一種預設的處理方式,如果使用者已經處理好了,也就不需要了,所以應該排在 onTouchListener 後面。(onTouchListener > onTouchEvent)

所以事件的排程順序應該是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener

下面我們來看一下實際測試結果:

手指按下,不移動,稍等片刻再擡起。

[Listener ]: onTouchListener      ACTION_DOWN
[GcsView  ]: onTouchEvent         ACTION_DOWN
[Listener ]: onLongClickListener  
[Listener ]: onTouchListener      ACTION_UP
[GcsView  ]: onTouchEvent         ACTION_UP
[Listener ]: onClickListener      

可以看到,測試結果也支援我們猜測的結論,因為長按 onLongClickListener 不需要 ACTION_UP 所以會在 ACTION_DOWN 之後就觸發。

接下來就看一下原始碼是怎麼設計的(省略了大量無關程式碼):

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    boolean result = false;  // result 為返回值,主要作用是告訴呼叫者事件是否已經被消費。
    if (onFilterTouchEventForSecurity(event)) {
        ListenerInfo li = mListenerInfo;
        /** 
         * 如果設定了OnTouchListener,並且當前 View 可點選,就呼叫監聽器的 onTouch 方法,
         * 如果 onTouch 方法返回值為 true,就設定 result 為 true。
         */
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        /** 
         * 如果 result 為 false,則呼叫自身的 onTouchEvent。
         * 如果 onTouchEvent 返回值為 true,則設定 result 為 true。
         */
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    ...
    return result;
}

如果覺得原始碼還是太長,那麼用虛擬碼實現應當是這樣的(省略若干安全判斷),簡單粗暴:

public boolean dispatchTouchEvent(MotionEvent event) {
  if (mOnTouchListener.onTouch(this, event)) {
      return true;
  } else if (onTouchEvent(event)) {
      return true;
  }
  return false;
}

正當你沉迷在原始碼的”精妙”邏輯的時候,你可能沒發現有兩個東西失蹤了,等回過神來,定睛一看,哎呦媽呀,OnClick 和 OnLongClick 去哪裡了?

不要擔心,OnClick 和 OnLongClick 的具體呼叫位置在 onTouchEvent 中,看原始碼(同樣省略大量無關程式碼):

public boolean onTouchEvent(MotionEvent event) {
    ...
    final int action = event.getAction();
    // 檢查各種 clickable
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                ...
                removeLongPressCallback();  // 移除長按
                ...
                performClick();             // 檢查單擊
                ...
                break;
            case MotionEvent.ACTION_DOWN:
                ...
                checkForLongClick(0);       // 檢測長按
                ...
                break;
            ...
        }
        return true;                        // ◀︎表示事件被消費
    }
    return false;
}

注意了,第一個重點要出現了(敲黑板)!

注意上面程式碼中存在一個 return true; 並且是隻要 View 可點選就返回 true,就表示事件被消費了。

舉個栗子: I have a RelativeLayout,I have a View,Ugh,RelativeLayout - View

<RelativeLayout
    android:background="#CCC"
    android:id="@+id/layout"
    android:onClick="myClick"
    android:layout_width="200dp"
    android:layout_height="200dp">
    <View
        android:clickable="true"
        android:layout_width="200dp"
        android:layout_height="200dp" />
</RelativeLayout>

現在你有了一個 RelativeLayout - View 你開開心心的為 RelativeLayout 設定了一個點選事件myClick,然而你會發現不論怎麼點都不會接收到資訊,仔細一看,發現內部的 View 有一個屬性 android:clickable="true" 正是這個看似不起眼的屬性把事件給消費掉了,由此我們可以得出如下結論:
1. 不論 View 自身是否註冊點選事件,只要 View 是可點選的就會消費事件。
2. 事件是否被消費由返回值決定,true 表示消費,false 表示不消費,與是否使用了事件無關。

關於 View 的事件分發先說這麼多,下面我們來看一下 ViewGroup 的事件分發。

ViewGroup 相關

ViewGroup(通常是各種Layout) 的事件分發相對來說就要麻煩一些,因為 ViewGroup 不僅要考慮自身,還要考慮各種 ChildView,一旦處理不好就容易引起各種事件衝突,正所謂養兒方知父母難啊。

VIewGroup 的事件分發流程又是如何的呢?

上一篇文章 事件分發機制原理 中我們瞭解到事件是通過ViewGroup一層一層傳遞的,最終傳遞給 View,ViewGroup 要比它的 ChildView 先拿到事件,並且有權決定是否告訴要告訴 ChildView。在預設的情況下 ViewGroup 事件分發流程是這樣的。

  • 1.判斷自身是否需要(詢問 onInterceptTouchEvent 是否攔截),如果需要,呼叫自己的 onTouchEvent。
  • 2.自身不需要或者不確定,則詢問 ChildView ,一般來說是呼叫手指觸控位置的 ChildView。
  • 3.如果子 ChildView 不需要則呼叫自身的 onTouchEvent。

用虛擬碼應該是這樣的:

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean result = false;             // 預設狀態為沒有消費過

    if (!onInterceptTouchEvent(ev)) {   // 如果沒有攔截交給子View
        result = child.dispatchTouchEvent(ev);
    }

    if (!result) {                      // 如果事件沒有被消費,詢問自身onTouchEvent
        result = onTouchEvent(ev);
    }

    return result;
}

有人看到這裡可能會有疑問,我看過原始碼,ViewGroup 的 dispatchTouchEvent 可有二百多行呢,你弄這幾行就想忽悠我,別以為我讀書少。

當然了,上述原始碼是不完善的,還有很多問題是沒有解決的,例如:

1. ViewGroup 中可能有多個 ChildView,如何判斷應該分配給哪一個?

這個很容易,就是把所有的 ChildView 遍歷一遍,如果手指觸控的點在 ChildView 區域內就分發給這個View。

2. 當該點的 ChildView 有重疊時應該如何分配?

當 ChildView 重疊時,一般會分配給顯示在最上面的 ChildView
如何判斷哪個是顯示在最上面的呢?後面載入的一般會覆蓋掉之前的,所以顯示在最上面的是最後載入的

如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" 
    android:id="@+id/activity_main"
    android:layout_width="match_parent" 
    android:layout_height="match_parent"
    tools:context="com.gcssloop.viewtest.MainActivity">
    <View
        android:id="@+id/view1"
        android:background="#E4A07B"
        android:layout_width="200dp"
        android:layout_height="200dp"/>
    <View
        android:id="@+id/view2"
        android:layout_margin="100dp"
        android:background="#BDDA66"
        android:layout_width="200dp"
        android:layout_height="200dp"/>
</RelativeLayout>

當手指點選有重疊區域時,分如下幾種情況:

  1. 只有 View1 可點選時,事件將會分配給 View1,即使被 View2 遮擋,這一部分仍是 View1 的可點選區域。
  2. 只有 View2 可點選時,事件將會分配給 View2。
  3. View1 和 View2 均可點選時,事件會分配給後加載的 View2,View2 將事件消費掉,View1接收不到事件。

注意:

  • 上面說的是可點選,可點選包括很多種情況,只要你給View註冊了 onClickListener、onLongClickListener、OnContextClickListener 其中的任何一個監聽器或者設定了 android:clickable=”true” 就代表這個 View 是可點選的。
    另外,某些 View 預設就是可點選的,例如,Button,CheckBox 等。
  • 給 View 註冊 OnTouchListener 不會影響 View 的可點選狀態。即使給 View 註冊 OnTouchListener ,只要不返回 true 就不會消費事件
3. ViewGroup 和 ChildView 同時註冊了事件監聽器(onClick等),哪個會執行?

事件優先給 ChildView,會被 ChildView消費掉,ViewGroup 不會響應。

4. 所有事件都應該被同一 View 消費

在上面的例子中我們分析後可以瞭解到,同一次點選事件只能被一個 View 消費,這是為什呢?主要是為了防止事件響應混亂,如果再一次完整的事件中分別將不同的事件分配給了不同的 View 容易造成事件響應混亂。

View 中 onClick 事件需要同時接收到 ACTION_DOWN 和 ACTION_UP 才能觸發,如果分配給了不同的 View,那麼 onClick 將無法被正確觸發。

安卓為了保證所有的事件都是被一個 View 消費的,對第一次的事件( ACTION_DOWN )進行了特殊判斷,View 只有消費了 ACTION_DOWN 事件,才能接收到後續的事件(可點選控制元件會預設消費所有事件),並且會將後續所有事件傳遞過來,不會再傳遞給其他 View,除非上層 View 進行了攔截。
如果上層 View 攔截了當前正在處理的事件,會收到一個 ACTION_CANCEL,表示當前事件已經結束,後續事件不會再傳遞過來。

原始碼:

其實如果能夠理解上面的內容,不看原始碼也能非常順利的使用事件分發,但原始碼中能挖掘出更多的內容。

public boolean dispatchTouchEvent(MotionEvent ev) {
    // 除錯用
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }

    // 判斷事件是否是針對可訪問的焦點檢視(很晚才新增的內容,個人猜測和螢幕輔助相關,方便盲人等使用裝置)
    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // 處理第一次ACTION_DOWN.
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 清除之前所有的狀態
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        // 檢查是否需要攔截.
        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);                        // 恢復操作,防止被更改
            } else {
                intercepted = false;
            }
        } else {
            // 沒有目標來處理該事件,而且也不是一個新的事件事件(ACTION_DOWN), 進行攔截。
            intercepted = true;
        }

        // 判斷事件是否是針對可訪問的焦點檢視
        if (intercepted || mFirstTouchTarget != null) {
            ev.setTargetAccessibilityFocus(false);
        }

        // 檢查事件是否被取消(ACTION_CANCEL).
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;

        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;

        // 如果沒有取消也沒有被攔截 (進入事件分發)
        if (!canceled && !intercepted) {

            // 如果事件是針對可訪問性焦點檢視,我們將其提供給具有可訪問性焦點的檢視。
            // 如果它不處理它,我們清除該標誌並像往常一樣將事件分派給所有的 ChildView。 
            // 我們檢測並避免保持這種狀態,因為這些事非常罕見。
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;

            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;

                // 清除此指標ID的早期觸控目標,防止不同步。
                removePointersFromTouchTargets(idBitsToAssign);

                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);  // 獲取觸控位置座標
                    final float y = ev.getY(actionIndex);
                    // 查詢可以接受事件的 ChildView
                    final ArrayList<View> preorderedList = buildOrderedChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    // ▼注意,從最後向前掃描
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = customOrder
                                ? getChildDrawingOrder(childrenCount, i) : i;
                        final View child = (preorderedList == null)
                                ? children[childIndex] : preorderedList.get(childIndex);

                        // 如果有一個檢視具有可訪問性焦點,我們希望它首先獲取事件,
                        // 如果不處理,我們將執行正常的分派。 
                        // 儘管這可能會分發兩次,但它能保證在給定的時間內更安全的執行。
                        if (childWithAccessibilityFocus != null) {
                            if (childWithAccessibilityFocus != child) {
                                continue;
                            }
                            childWithAccessibilityFocus = null;
                            i = childrenCount - 1;
                        }

                        // 檢查View是否允許接受事件(即處於顯示狀態(VISIBLE)或者正在播放動畫)
                        // 檢查觸控位置是否在View區域內
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        // getTouchTarget 中判斷了 child 是否包含在 mFirstTouchTarget 中
                        // 如果有返回 target,如果沒有返回 null 
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            // ChildView 已經準備好接受在其區域內的事件。
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;  // ◀︎已經找到目標View,跳出迴圈
                        }

                        resetCancelNextUpFlag(child);
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            mLastTouchDownTime = ev.getDownTime();
                            if (preorderedList != null) {
                                for (int j = 0; j < childrenCount; j++) {
                                    if (children[childIndex] == mChildren[j]) {
                                        mLastTouchDownIndex = j;
                                        break;
                                    }
                                }
                            } else {
                                mLastTouchDownIndex = childIndex;
                            }
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }

                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }

                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // 沒有找到 ChildView 接收事件
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }

        // 分發 TouchTarget
        if (mFirstTouchTarget == null) {
            // 沒有 TouchTarget,將當前 ViewGroup 當作普通的 View 處理。
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 分發TouchTarget,如果我們已經分發過,則避免分配給新的目標。 
            // 如有必要,取消分發。
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = 
            
           

相關推薦

定義View-事件分發機制

Android 事件分發機制詳解,在上一篇文章 事件分發機制原理 中簡要分析了一下事件分發機制的原理,原理是十分簡單的,一句話就能總結:責任鏈模式,事件層層傳遞,直到被消費。 雖然原理簡單,但是隨著 Android 不斷的發展,實際運用場景也越來越複雜,所以想要徹底玩轉事件分發機制還

定義View-事件分發機制原理

之前講解了很多與View繪圖相關的知識,你可以在 安卓自定義View教程目錄 中檢視到這些文章,如果你理解了這些文章,那麼至少2D繪圖部分不是難題了,大部分的需求都能滿足,但是關於View還有很多知識點,例如: 讓繪圖更加炫酷的Paint,讓View動起來的動畫

定義View-多點觸控

Android 多點觸控詳解,在前面的幾篇文章中我們大致瞭解了 Android 中的事件處理流程和一些簡單的處理方案,本次帶大家瞭解 Android 多點觸控相關的一些知識。 多點觸控 ( Multitouch,也稱 Multi-touch ),即同時接受螢幕上多個點的人機互動

定義View-特殊控制元件的事件處理方案

本文帶大家瞭解 Android 特殊形狀控制元件的事件處理方式,主要是利用了 Region 和 Matrix 的一些方法,超級實用的事件處理方案,相信看完本篇之後,任何奇葩控制元件的事件處理都會變得十分簡單。 不得不說,Android 對事件體系封裝的非常棒,即便對事件體系不太

定義View-手勢檢測(GestureDecetor)

Android 手勢檢測,主要是 GestureDetector 相關內容的用法和注意事項,本文依舊屬於事件處理這一體系,部分內容會涉及到之前文章提及過的知識點,如果你沒看過之前的文章,可以到 自定義 View 系列 來檢視這些內容。 在開發 Android 手機應用過程中,可

定義View-MotionEvent

Android MotionEvent 詳解,之前用了兩篇文章 事件分發機制原理 和 事件分發機制詳解 來講解事件分發,而作為事件分發主角之一的 MotionEvent 並沒有過多的說明,本文就帶大家瞭解 MotionEvent 的相關內容,簡要介紹觸控事件,主要包括 單點觸控、多點

定義View-Matrix Camera

本篇依舊屬於Matrix,主要講解Camera,Android下有很多相機應用,其中的美顏相機更是不少,不過今天這個Camera可不是我們平時拍照的那個相機,而是graphic包下的Camera,專業給View拍照的相機,不過既然是相機,作用都是類似的,主要是將3D的內容拍扁變成2D

定義View-Matrix

這應該是目前最詳細的一篇講解Matrix的中文文章了,在上一篇文章Matrix原理中,我們對Matrix做了一個簡單的瞭解,偏向理論,在本文中則會詳細的講解Matrix的具體用法,以及與Matrix相關的一些實用技巧。 ⚠️ 警告:測試本文章示例之前請關閉硬體加速。

定義View-Matrix原理

本文內容偏向理論,和 畫布操作 有重疊的部分,本文會讓你更加深入的瞭解其中的原理。 本篇的主角Matrix,是一個一直在後臺默默工作的勞動模範,雖然我們所有看到View背後都有著Matrix的功勞,但我們卻很少見到它,本篇我們就看看它是何方神聖吧。 由於Goog

定義View-PathMeasure

可以看到,在經過 Path之基本操作 Path之貝塞爾曲線 和 Path之完結篇 後, Path中各類方法基本上都講完了,表格中還沒有講解到到方法就是矩陣變換了,難道本篇終於要講矩陣了? 非也,矩陣這一部分仍在後面單獨講解,本篇主要講解 PathMeasure 這個類與 Path 的

定義View-Path之貝塞爾曲線

在上一篇文章Path之基本操作中我們瞭解了Path的基本使用方法,本次瞭解Path中非常非常非常重要的內容-貝塞爾曲線。 一.Path常用方法表 為了相容性(偷懶) 本表格中去除了在API21(即安卓版本5.0)以上

定義View-Path之基本操作

在上一篇Canvas之圖片文字中我們瞭解瞭如何使用Canvas中繪製圖片文字,結合前幾篇文章,Canvas的基本操作已經差不多完結了,然而Canvas不僅僅具有這些基本的操作,還可以更加炫酷,本次會了解到path(路徑)這個Canvas中的神器,有了這個神器,就能創造出更多炫(zhu

定義View-Canvas之圖片文字

在上一篇文章Canvas之畫布操作中我們瞭解了畫布的一些基本操作方法,本次瞭解一些繪製圖片文字相關的內容。如果你對前幾篇文章講述的內容熟練掌握的話,那麼恭喜你,本篇結束之後,大部分的自定義View已經難不倒你了,當然了,這並不是終點,接下來還會有更加炫酷的技能。 一.Canva

定義View-分類與流程

本章節為什麼要叫進階篇?(雖然講的是基礎內容),因為從本篇開始,將會逐漸揭開自定義View的神祕面紗,每一篇都將比上一篇內容更加深入,利用所學的知識能夠製作更加炫酷自定義View,就像在臺階上一樣,每一篇都更上一層,幫助大家一步步走向人生巔峰,出任CEO,迎娶白富美。 誤

定義View-Path之完結篇

經歷過前兩篇 Path之基本操作 和 Path之貝塞爾曲線 的講解,本篇終於進入Path的收尾篇,本篇結束後Path的大部分相關方法都已經講解完了,但Path還有一些更有意思的玩法,應該會在後續的文章中出現。 一.Path常用方法表 為了相容性(偷懶) 本表格中去除

定義View-縮放手勢檢測(ScaleGestureDecetor)

0. 前言 Android 縮放手勢檢測,ScaleGestureDetector 相關內容的用法和注意事項,本文依舊屬於事件處理這一體系,在大多數的情況下,縮放手勢都不是單獨存在的,需要配合其它的手勢來使用,所以推薦配合 手勢檢測(GestureDetector) 一

定義View-Canvas之畫布操作

Canvas之畫布操作 上一篇Canvas之繪製基本形狀中我們瞭解瞭如何使用Canvas繪製基本圖形,本次瞭解一些基本的畫布操作。 本來想把畫布操作放到後面部分的,但是發現很多圖形繪製都離不開畫布操作,於是先講解一下畫布的基本操作方法。

定義 View :貝塞爾曲線

在上一篇文章Path之基本圖形中我們瞭解了Path的基本使用方法,本次瞭解Path中非常非常非常重要的內容-貝塞爾曲線。 一.Path常用方法表 為了相容性(偷懶) 本表格中去除了在API21(即安卓版本5.0)以上才新增的方法。忍不住吐槽一下,為啥看起來有些順手就能寫的

定義View:Path基本操作

在上一篇Canvas之圖片文字中我們瞭解瞭如何使用Canvas中繪製圖片文字,結合前幾篇文章,Canvas的基本操作已經差不多完結了,然而Canvas不僅僅具有這些基本的操作,還可以更加炫酷,本次會了解到path(路徑)這個Canvas中的神器,有了這個神器,就能創造出更多炫(

定義View: 圖片文字

在上一篇文章Canvas之畫布操作中我們瞭解了畫布的一些基本操作方法,本次瞭解一些繪製圖片文字相關的內容。如果你對前幾篇文章講述的內容熟練掌握的話,那麼恭喜你,本篇結束之後,大部分的自定義View已經難不倒你了,當然了,這並不是終點,接下來還會有更加炫酷的技能。 一.Canva