<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                # 特殊控件的事件處理方案 本文帶大家了解 Android 特殊形狀控件的事件處理方式,主要是利用了 Region 和 Matrix 的一些方法,超級實用的事件處理方案,相信看完本篇之后,任何奇葩控件的事件處理都會變得十分簡單。 不得不說,Android 對事件體系封裝的非常棒,即便對事件體系不太了解的人,只要簡單的調用方法就能使用,而且具有防呆設計,能夠保證事件流的完整性和統一性,最大可能性的避免了事件處理的混亂,著實令人佩服。 **然而世界上并沒有絕對完美的東西,當"事件處理"遇上"自定義View",一場好戲就開演了。** ## 特殊形狀控件 在通常的情況下,自定義 View 直接使用系統的事件體系處理就行,我們也不需要特殊處理,然而當一些特殊的控件出現的時候,麻煩就來了,舉個栗子: ![](http://ww2.sinaimg.cn/large/005Xtdi2jw1f9t9u0tignj308c08cq3f.jpg) 這是一個在遙控器上非常常見的按鍵布局,注意中間上下左右選擇的部分,看起來十分簡單,然而當你真正準備在手機上實現的時候麻煩就出現了。因為所有的 View 默認都是矩形的,所以事件接收區域也是矩形的,如果直接使用系統提供的 View 來組合出一摸一樣的布局也很簡單,但點擊區域該如何處理?顯然有部分點擊區域是在控件外面的,并且會產生重疊區域: > 紅色方框表示 View 的可點擊區域。 ![](http://ww3.sinaimg.cn/large/005Xtdi2jw1f9ta3eymeej308c08cwf1.jpg) 當我們面對這樣比較奇特的控件的時候,有很多處理辦法,比較投機的一種就是背景貼一個靜態圖,按鈕做成透明的,設置小一點,放在對應的位置,這樣可以保證不會誤觸,當然了如果想要點擊效果可以在按鈕按下的時候更新一下背景圖,這樣雖然也可以,但是這樣會導致可點擊區域變小,體驗效果變差,設計方案變得復雜,而且邏輯也不容易處理,是一種非常糟糕的設計。 當然了,看了我這么多文章的小伙伴應該也猜到接下來要說什么了,沒錯,就是自定義 View。當我們面對一些奇葩控件的時候,自定義 View 就變成了一種非常好用的處理方案。 相信小伙伴們看過 [前面的文章][CustomViewIndex] 之后,對各種圖形的繪制已經不成問題了,所以我們直接處理重點問題。 > #### 注意: > > 本文中所有的 自定義View 均繼承自 CustomView ,這是一個自定義的超類,目的是簡化 自定義View 部分常用操作,你可以在 [ViewSupport](https://github.com/GcsSloop/ViewSupport/wiki/CustomView) 中找到它以及關于它的簡介。 > **?? 警告:測試本文章示例之前請關閉硬件加速。** ## 特殊形狀控的點擊區域判斷 要進行特殊形狀的點擊判斷,要用到一個之前沒有使用過的類:Region。 Region 直接翻譯的意思是 地域,區域。**在此處應該是區域的意思**。它和 Path 有些類似,但 Path 可以是不封閉圖形,而 Region 總是封閉的。可以通過 `setPath` 方法將 Path 轉換為 Region。 **本文中我們重點要使用到的是 Region 中的 `contains` 方法,這個方法可以判斷一個點是否包含在該區域內。** 接下來是一個簡單的示例,**判斷手指是否是在圓形區域內按下**: ![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f9xtlae5wzj308c0ea3yn.jpg) 代碼: ```java public class RegionClickView extends CustomView { Region circleRegion; Path circlePath; public RegionClickView(Context context) { super(context); mDeafultPaint.setColor(0xFF4E5268); circlePath = new Path(); circleRegion = new Region(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // ▼在屏幕中間添加一個圓 circlePath.addCircle(w/2, h/2, 300, Path.Direction.CW); // ▼將剪裁邊界設置為視圖大小 Region globalRegion = new Region(-w, -h, w, h); // ▼將 Path 添加到 Region 中 circleRegion.setPath(circlePath, globalRegion); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN: int x = (int) event.getX(); int y = (int) event.getY(); // ▼點擊區域判斷 if (circleRegion.contains(x,y)){ Toast.makeText(this.getContext(),"圓被點擊",Toast.LENGTH_SHORT).show(); } break; } return true; } @Override protected void onDraw(Canvas canvas) { // ▼注意此處將全局變量轉化為局部變量,方便 GC 回收 canvas Path circle = circlePath; // 繪制圓 canvas.drawPath(circle,mDeafultPaint); } } ``` > 代碼中比較重要的內容都用 ▼ 符號標記出來了。 上述代碼非常簡單,就是創建了個 Path 并在其中添加圓形,之后將 Path 設置到 Region 中,當手指在屏幕上按下的時候判斷手指位置是否在 Region 區域內。 ## 畫布變換后坐標轉換問題 還是本文一開始的例子,繪制一個上下左右選擇按鍵,這個控件是上下左右對稱的,熟悉我代碼風格的小伙伴都知道,如果遇上這種問題,我肯定是要將坐標系平移到這個控件中心的,這樣數據比較好計算,然而進行畫布變換操作會產生一個新問題:**手指觸摸的坐標系和畫布坐標系不統一,就可能引起手指觸摸位置和繪制位置不統一。** 舉個栗子: > 畫布移動后在手指按下位置繪制一個圓,可以看到,直接拿手指觸摸位置的坐標來繪制會導致繪制位置不正確,**兩者坐標是相同的,但是由于坐標系不同,導致實際顯示位置不同。** ![](http://ww4.sinaimg.cn/large/005Xtdi2jw1f9tdc3p84uj308c0d4mx9.jpg) 代碼: ```java public class CanvasVonvertTouchTest extends CustomView{ float down_x = -1; float down_y = -1; public CanvasVonvertTouchTest(Context context) { this(context, null); } public CanvasVonvertTouchTest(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getActionMasked()){ case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: down_x = event.getX(); down_y = event.getY(); invalidate(); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: down_x = down_y = -1; invalidate(); break; } return true; } @Override protected void onDraw(Canvas canvas) { float x = down_x; float y = down_y; drawTouchCoordinateSpace(canvas); // 繪制觸摸坐標系 灰色 // ▼注意畫布平移 canvas.translate(mViewWidth/2, mViewHeight/2); drawTranslateCoordinateSpace(canvas); // 繪制平移后的坐標系,紅色 if (x == -1 && y == -1) return; // 如果沒有就返回 canvas.drawCircle(x,y,20,mDeafultPaint); // 在觸摸位置繪制一個小圓 } /** * 繪制觸摸坐標系,灰色,為了能夠顯示出坐標系,將坐標系位置稍微偏移了一點 */ private void drawTouchCoordinateSpace(Canvas canvas) { canvas.save(); canvas.translate(10,10); CanvasAidUtils.set2DAxisLength(1000, 0, 1400, 0); CanvasAidUtils.setLineColor(Color.GRAY); CanvasAidUtils.draw2DCoordinateSpace(canvas); canvas.restore(); } /** * 繪制平移后的坐標系,紅色 */ private void drawTranslateCoordinateSpace(Canvas canvas) { CanvasAidUtils.set2DAxisLength(500, 500, 700, 700); CanvasAidUtils.setLineColor(Color.RED); CanvasAidUtils.draw2DCoordinateSpace(canvas); CanvasAidUtils.draw2DCoordinateSpace(canvas); } } ``` **那么問題來了,我們在之前的文章中講過,映射不同坐標系的坐標用 什么來著?** **是 Matrix。** 如果看過我之前的文章但沒有想起來的說明你們根本沒有認真看,全部拖出去糟蹋 5 分鐘! 沒看過的點 [Matrix原理][Matrix_Basic] 和 [Matrix詳解][Matrix_Method] 。 > **Matrix 是一個矩陣,主要功能是坐標映射,數值轉換。** 那么接下來我們就對上面的示例進行簡單的改造一下,讓觸摸位置和實際繪制繪制重合。小白點和黑色的圓沒有完全重合是因為系統顯示觸摸位置的繪制邏輯和我使用的繪制邏輯不太相同導致的。 ![](http://ww3.sinaimg.cn/large/005Xtdi2jw1f9te2mzxcvj308c0d40st.jpg) 代碼: **注意:比較重要的修改位置用▼標記出來了。** ```java public class CanvasVonvertTouchTest extends CustomView{ float down_x = -1; float down_y = -1; public CanvasVonvertTouchTest(Context context) { this(context, null); } public CanvasVonvertTouchTest(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getActionMasked()){ case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: // ▼ 注意此處使用 getRawX,而不是 getX down_x = event.getRawX(); down_y = event.getRawY(); invalidate(); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: down_x = down_y = -1; invalidate(); break; } return true; } @Override protected void onDraw(Canvas canvas) { float[] pts = {down_x, down_y}; drawTouchCoordinateSpace(canvas); // 繪制觸摸坐標系,灰色 // ▼注意畫布平移 canvas.translate(mViewWidth/2, mViewHeight/2); drawTranslateCoordinateSpace(canvas); // 繪制平移后的坐標系,紅色 if (pts[0] == -1 && pts[1] == -1) return; // 如果沒有就返回 // ▼ 獲得當前矩陣的逆矩陣 Matrix invertMatrix = new Matrix(); canvas.getMatrix().invert(invertMatrix); // ▼ 使用 mapPoints 將觸摸位置轉換為畫布坐標 invertMatrix.mapPoints(pts); // 在觸摸位置繪制一個小圓 canvas.drawCircle(pts[0],pts[1],20,mDeafultPaint); } /** * 繪制觸摸坐標系,顏色為灰色,為了能夠顯示出坐標系,將坐標系位置稍微偏移了一點 */ private void drawTouchCoordinateSpace(Canvas canvas) { canvas.save(); canvas.translate(10,10); CanvasAidUtils.set2DAxisLength(1000, 0, 1400, 0); CanvasAidUtils.setLineColor(Color.GRAY); CanvasAidUtils.draw2DCoordinateSpace(canvas); canvas.restore(); } /** * 繪制平移后的坐標系,顏色為紅色 */ private void drawTranslateCoordinateSpace(Canvas canvas) { CanvasAidUtils.set2DAxisLength(500, 500, 700, 700); CanvasAidUtils.setLineColor(Color.RED); CanvasAidUtils.draw2DCoordinateSpace(canvas); CanvasAidUtils.draw2DCoordinateSpace(canvas); } } ``` 其實核心部分就這兩點: ```java // ▼ 注意此處使用 getRawX,而不是 getX down_x = event.getRawX(); down_y = event.getRawY(); // ------------------------------------- // ▼ 獲得當前矩陣的逆矩陣 Matrix invertMatrix = new Matrix(); canvas.getMatrix().invert(invertMatrix); // ▼ 使用 mapPoints 將觸摸位置轉換為畫布坐標 invertMatrix.mapPoints(pts); ``` 1. 使用全局坐標系 2. 使用逆矩陣的 mapPoints **原理嘛,其實非常簡單,我們在畫布上正常的繪制,需要將畫布坐標系轉換為全局坐標系后才能真正的繪制內容。所以我們反著來,將獲得到的全局坐標系坐標使用當前畫布的逆矩陣轉化一下,就轉化為當前畫布的坐標系坐標了,如果對 [Matrix原理][Matrix_Basic] 和 [Matrix詳解][Matrix_Method] 理解了,即便我不說你們也肯定會想到這個方案的。** ## 仿遙控器按鈕代碼示例 在解決了上述兩大難題之后,相信不論形狀如何奇葩的自定義控件,基本上都難不倒大家了,最后用一個簡單的示例作為結尾,還是文章開頭所舉的例子,核心內容就是上面講的兩個東西。 ![](http://ww1.sinaimg.cn/large/005Xtdi2jw1f9tinrk6ilj308c0eat92.jpg) 代碼: ```java public class RemoteControlMenu extends CustomView { Path up_p, down_p, left_p, right_p, center_p; Region up, down, left, right, center; Matrix mMapMatrix = null; int CENTER = 0; int UP = 1; int RIGHT = 2; int DOWN = 3; int LEFT = 4; int touchFlag = -1; int currentFlag = -1; MenuListener mListener = null; int mDefauColor = 0xFF4E5268; int mTouchedColor = 0xFFDF9C81; public RemoteControlMenu(Context context) { this(context, null); } public RemoteControlMenu(Context context, AttributeSet attrs) { super(context, attrs); up_p = new Path(); down_p = new Path(); left_p = new Path(); right_p = new Path(); center_p = new Path(); up = new Region(); down = new Region(); left = new Region(); right = new Region(); center = new Region(); mDeafultPaint.setColor(mDefauColor); mDeafultPaint.setAntiAlias(true); mMapMatrix = new Matrix(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mMapMatrix.reset(); // 注意這個區域的大小 Region globalRegion = new Region(-w, -h, w, h); int minWidth = w > h ? h : w; minWidth *= 0.8; int br = minWidth / 2; RectF bigCircle = new RectF(-br, -br, br, br); int sr = minWidth / 4; RectF smallCircle = new RectF(-sr, -sr, sr, sr); float bigSweepAngle = 84; float smallSweepAngle = -80; // 根據視圖大小,初始化 Path 和 Region center_p.addCircle(0, 0, 0.2f * minWidth, Path.Direction.CW); center.setPath(center_p, globalRegion); right_p.addArc(bigCircle, -40, bigSweepAngle); right_p.arcTo(smallCircle, 40, smallSweepAngle); right_p.close(); right.setPath(right_p, globalRegion); down_p.addArc(bigCircle, 50, bigSweepAngle); down_p.arcTo(smallCircle, 130, smallSweepAngle); down_p.close(); down.setPath(down_p, globalRegion); left_p.addArc(bigCircle, 140, bigSweepAngle); left_p.arcTo(smallCircle, 220, smallSweepAngle); left_p.close(); left.setPath(left_p, globalRegion); up_p.addArc(bigCircle, 230, bigSweepAngle); up_p.arcTo(smallCircle, 310, smallSweepAngle); up_p.close(); up.setPath(up_p, globalRegion); } @Override public boolean onTouchEvent(MotionEvent event) { float[] pts = new float[2]; pts[0] = event.getRawX(); pts[1] = event.getRawY(); mMapMatrix.mapPoints(pts); int x = (int) pts[0]; int y = (int) pts[1]; switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: touchFlag = getTouchedPath(x, y); currentFlag = touchFlag; break; case MotionEvent.ACTION_MOVE: currentFlag = getTouchedPath(x, y); break; case MotionEvent.ACTION_UP: currentFlag = getTouchedPath(x, y); // 如果手指按下區域和抬起區域相同且不為空,則判斷點擊事件 if (currentFlag == touchFlag && currentFlag != -1 && mListener != null) { if (currentFlag == CENTER) { mListener.onCenterCliched(); } else if (currentFlag == UP) { mListener.onUpCliched(); } else if (currentFlag == RIGHT) { mListener.onRightCliched(); } else if (currentFlag == DOWN) { mListener.onDownCliched(); } else if (currentFlag == LEFT) { mListener.onLeftCliched(); } } touchFlag = currentFlag = -1; break; case MotionEvent.ACTION_CANCEL: touchFlag = currentFlag = -1; break; } invalidate(); return true; } // 獲取當前觸摸點在哪個區域 int getTouchedPath(int x, int y) { if (center.contains(x, y)) { return 0; } else if (up.contains(x, y)) { return 1; } else if (right.contains(x, y)) { return 2; } else if (down.contains(x, y)) { return 3; } else if (left.contains(x, y)) { return 4; } return -1; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.translate(mViewWidth / 2, mViewHeight / 2); // 獲取測量矩陣(逆矩陣) if (mMapMatrix.isIdentity()) { canvas.getMatrix().invert(mMapMatrix); } // 繪制默認顏色 canvas.drawPath(center_p, mDeafultPaint); canvas.drawPath(up_p, mDeafultPaint); canvas.drawPath(right_p, mDeafultPaint); canvas.drawPath(down_p, mDeafultPaint); canvas.drawPath(left_p, mDeafultPaint); // 繪制觸摸區域顏色 mDeafultPaint.setColor(mTouchedColor); if (currentFlag == CENTER) { canvas.drawPath(center_p, mDeafultPaint); } else if (currentFlag == UP) { canvas.drawPath(up_p, mDeafultPaint); } else if (currentFlag == RIGHT) { canvas.drawPath(right_p, mDeafultPaint); } else if (currentFlag == DOWN) { canvas.drawPath(down_p, mDeafultPaint); } else if (currentFlag == LEFT) { canvas.drawPath(left_p, mDeafultPaint); } mDeafultPaint.setColor(mDefauColor); } public void setListener(MenuListener listener) { mListener = listener; } // 點擊事件監聽器 public interface MenuListener { void onCenterCliched(); void onUpCliched(); void onRightCliched(); void onDownCliched(); void onLeftCliched(); } } ``` **運行效果:** 當手指在某一區域活動時,該區域會高亮顯示,如果注冊了監聽器,點擊某一區域會觸發監聽器回調。 ## 關于硬件加速的問題 **硬件加速是個好東西,但是處理不好會引起諸多問題,博主為了怕麻煩我一直關閉硬件加速。** 然而硬件加速在 Android 4.0 以上是默認開啟的,這就導致了有好幾位魔法師反饋測試結果和我的測試結果不同,我來簡單說明一下硬件加速干了什么事情,以及這些文章中的鍋是如何產生的,應該由誰來背。 我在 [Matrix 原理][Matrix_Basic] 中說過 Matrix 的作用: **Matrix作用就是坐標映射。** 其核心功能就是將單個 View 的坐標系轉化為屏幕(物理)坐標系,雖然轉換一次費不了多少時間,但是當執行動畫效果等需要大量快速重繪的情況下,耗費的時間就需要考量一下了,于是乎,硬件加速干了一件非常**精明**的事情,**把所有畫布坐標系都設置為屏幕(物理)坐標系**,之后在 View 繪制區域設置一個遮罩,保證繪制內容不會超過 View 自身的大小,**這樣就直接跳過坐標轉換過程,可以節省坐標系之間數值轉換耗費的時間**。因此導致了以下問題: 1. 開啟硬件加速情況下 event.getX() 和 不開啟情況下 event.getRawX() 等價,獲取到的是屏幕(物理)坐標 (本文的鍋)。 2. 開啟硬件加速情況下 event.getRawX() 數值是一個錯誤數值,因為本身就是全局的坐標又疊加了一次 View 的偏移量,所以肯定是不正確的 (本文的鍋)。 3. 從 Canvas 獲取到的 Matrix 是全局的,默認情況下 x,y 偏移量始終為0,因此你不能從這里拿到當前 View 的偏移量 ( Matrix系列文章中的鍋 )。 4. 由于其使用的是遮罩來控制繪制區域,所以如果重繪 path 時,如果 path 區域變大,但沒有執行單步操作會導致 path 繪制不完整或者看起來比較奇怪 (Path系列文章中的鍋)。 很顯然,這個硬件加速有點6,制造了各種鍋想讓我來背,然而智慧的我早已看穿一切,默默的把硬件加速關閉了,因為我不知道它還有多少鍋沒亮出來。 **這里順便挖個坑,等我搞明白硬件加速扔鍋的邏輯之后,專門寫一篇硬件加速的文章,把硬件加速的鍋全埋進去,再也不背這口大黑鍋了**。 **(╯°Д°)╯︵ ┻━┻** **個人建議:** 1. APP全局關閉硬件加速。 2. 針對動畫較多的 Activity 或者 View 單獨開啟硬件加速。 3. 如果應用要兼容到 3.0 以下,不要使用硬件加速的特性,或者進行兼容處理。 4. 如果 自定義View 出現與繪圖相關的異常,請務必檢查一下硬件加速。 5. 如果想關掉硬件加速看這里: [Android如何關閉硬件加速](https://github.com/GcsSloop/AndroidNote/issues/7) 。 ## 總結 本文雖然代碼比較多,但核心概念非常簡單,主要涉及以下兩點: 1. Region 的區域檢測。 2. Matrix 的坐標映射。 **這兩個知識點都不是很難,然而靈活運用起來卻是非常強大的,如果有對 Matrix 不了解的小伙伴,推薦去看我 [之前的文章][CustomViewIndex],里面有關于Matrix的詳細介紹,** ## About Me ### 作者微博: <a href="http://weibo.com/GcsSloop" target="_blank">@GcsSloop</a> <a href="http://www.gcssloop.com/info/about" target="_blank"><img src="http://ww4.sinaimg.cn/large/005Xtdi2gw1f1qn89ihu3j315o0dwwjc.jpg" width="300" style="display:inline;" /></a> [CustomViewIndex]: http://www.gcssloop.com/customview/CustomViewIndex [Matrix_Basic]: http://www.gcssloop.com/customview/Matrix_Basic [Matrix_Method]: http://www.gcssloop.com/customview/Matrix_Method
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看