在 Android 中實現支持多手勢交互的自定義 View(Kotlin 完整指南)

本文將手把手教你創建一個支持拖動、縮放、旋轉等多種手勢交互的自定義 View,并提供完整的代碼實現和優化建議。

一、基礎實現

1.1 創建自定義 View 骨架

import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.*class InteractiveView @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {// 繪制相關private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {color = Color.BLUEstyle = Paint.Style.FILL}private var circleRadius = 100fprivate val originalRect = RectF()// 變換參數private var offsetX = 0fprivate var offsetY = 0fprivate var scaleFactor = 1fprivate var rotationAngle = 0f// 手勢檢測器private val gestureDetector: GestureDetectorprivate val scaleDetector: ScaleGestureDetectorprivate val rotationDetector: RotationGestureDetectorinit {// 初始化手勢檢測器gestureDetector = GestureDetector(context, GestureListener())scaleDetector = ScaleGestureDetector(context, ScaleListener())rotationDetector = RotationGestureDetector(context, RotationListener())}override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)originalRect.set(0f, 0f, w.toFloat(), h.toFloat())}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)canvas.save()// 應用變換canvas.translate(offsetX, offsetY)canvas.scale(scaleFactor, scaleFactor, pivotX, pivotY)canvas.rotate(rotationAngle, pivotX, pivotY)// 繪制內容canvas.drawCircle(originalRect.centerX(),originalRect.centerY(),circleRadius,paint)canvas.restore()}override fun onTouchEvent(event: MotionEvent): Boolean {scaleDetector.onTouchEvent(event)rotationDetector.onTouchEvent(event)gestureDetector.onTouchEvent(event)return true}// 其他實現將在下文展開...
}

1.2 實現基本手勢

拖動處理:
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {override fun onScroll(e1: MotionEvent,e2: MotionEvent,distanceX: Float,distanceY: Float): Boolean {offsetX -= distanceXoffsetY -= distanceYapplyBoundaryConstraints()invalidate()return true}override fun onDoubleTap(e: MotionEvent): Boolean {// 雙擊重置變換resetTransformations()invalidate()return true}
}private fun resetTransformations() {offsetX = 0foffsetY = 0fscaleFactor = 1frotationAngle = 0f
}
縮放處理:
private var pivotX = 0f
private var pivotY = 0fprivate inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {pivotX = detector.focusXpivotY = detector.focusYreturn true}override fun onScale(detector: ScaleGestureDetector): Boolean {val scale = detector.scaleFactorscaleFactor *= scalescaleFactor = scaleFactor.coerceIn(0.1f, 5f)// 調整中心點偏移offsetX += (pivotX - offsetX) * (1 - scale)offsetY += (pivotY - offsetY) * (1 - scale)invalidate()return true}
}

二、高級手勢實現

2.1 旋轉手勢檢測器

class RotationGestureDetector(context: Context,private val listener: OnRotationGestureListener
) {private var prevAngle = 0ffun onTouchEvent(event: MotionEvent): Boolean {if (event.pointerCount != 2) return falsewhen (event.actionMasked) {MotionEvent.ACTION_POINTER_DOWN -> {prevAngle = getAngle(event)}MotionEvent.ACTION_MOVE -> {val newAngle = getAngle(event)listener.onRotate(newAngle - prevAngle)prevAngle = newAngle}}return true}private fun getAngle(event: MotionEvent): Float {val dx = event.getX(0) - event.getX(1)val dy = event.getY(0) - event.getY(1)return Math.toDegrees(atan2(dy.toDouble(), dx.toDouble())).toFloat()}interface OnRotationGestureListener {fun onRotate(angleDelta: Float)}
}// 在 InteractiveView 中添加:
private inner class RotationListener : RotationGestureDetector.OnRotationGestureListener {override fun onRotate(angleDelta: Float) {rotationAngle += angleDeltarotationAngle %= 360invalidate()}
}

2.2 邊界約束

private fun applyBoundaryConstraints() {val scaledWidth = originalRect.width() * scaleFactorval scaledHeight = originalRect.height() * scaleFactorval maxOffsetX = (scaledWidth - originalRect.width()) / 2val maxOffsetY = (scaledHeight - originalRect.height()) / 2offsetX = offsetX.coerceIn(-maxOffsetX, maxOffsetX)offsetY = offsetY.coerceIn(-maxOffsetY, maxOffsetY)
}

三、性能優化實現

慣性滑動

private val scroller = Scroller(context)override fun computeScroll() {if (scroller.computeScrollOffset()) {offsetX = scroller.currX.toFloat()offsetY = scroller.currY.toFloat()applyBoundaryConstraints()invalidate()}
}private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {override fun onFling(e1: MotionEvent,e2: MotionEvent,velocityX: Float,velocityY: Float): Boolean {scroller.fling(offsetX.toInt(),offsetY.toInt(),velocityX.toInt(),velocityY.toInt(),Int.MIN_VALUE,Int.MAX_VALUE,Int.MIN_VALUE,Int.MAX_VALUE)invalidate()return true}
}

四、完整布局示例

<com.example.app.InteractiveViewandroid:id="@+id/interactiveView"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#F0F0F0"/>

五、最佳實踐建議

  1. 繪制優化:

    override fun onDraw(canvas: Canvas) {// 避免在繪制過程中創建新對象canvas.drawCircle(originalRect.centerX(),originalRect.centerY(),circleRadius,paint // 重用預定義的 Paint 對象)
    }
    
  2. 手勢優先級處理:

    override fun onTouchEvent(event: MotionEvent): Boolean {when {scaleDetector.isInProgress -> scaleDetector.onTouchEvent(event)rotationDetector.isInProgress -> rotationDetector.onTouchEvent(event)else -> gestureDetector.onTouchEvent(event)}return true
    }
    
  3. 多指觸控處理:

    private var activePointerId = MotionEvent.INVALID_POINTER_IDoverride fun onTouchEvent(event: MotionEvent): Boolean {when (event.actionMasked) {MotionEvent.ACTION_POINTER_DOWN -> {activePointerId = event.getPointerId(event.actionIndex)}MotionEvent.ACTION_POINTER_UP -> {val remainingPointer = if (event.actionIndex == 0) 1 else 0activePointerId = event.getPointerId(remainingPointer)}}// 其他手勢處理...
    }
    

六、完整功能集成

最終的自定義 View 將支持:

  • 單指拖動
  • 雙指縮放(帶錨點)
  • 雙指旋轉
  • 慣性滑動
  • 雙擊重置
  • 邊界約束
  • 多指觸控支持

可以通過組合這些手勢實現復雜的交互效果,如圖片查看器、可操作圖表等。


以下是為您整合所有功能的完整實現代碼:

import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.*
import android.widget.OverScroller
import kotlin.math.*class AdvancedInteractiveView @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {// 繪制參數private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {color = Color.BLUEstyle = Paint.Style.FILL}private var circleRadius = 100fprivate val originalRect = RectF()// 變換參數private var offsetX = 0fprivate var offsetY = 0fprivate var scaleFactor = 1fprivate var rotationAngle = 0fprivate var pivotX = 0fprivate var pivotY = 0f// 手勢檢測器private val gestureDetector: GestureDetectorprivate val scaleDetector: ScaleGestureDetectorprivate val rotationDetector: RotationGestureDetectorprivate val scroller = OverScroller(context)// 邊界約束參數private var minScale = 0.5fprivate var maxScale = 5fprivate var isScaling = falseprivate var activePointerId = MotionEvent.INVALID_POINTER_IDinit {gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {override fun onScroll(e1: MotionEvent,e2: MotionEvent,distanceX: Float,distanceY: Float): Boolean {if (!isScaling) {offsetX -= distanceXoffsetY -= distanceYapplyBoundaryConstraints()invalidate()}return true}override fun onDoubleTap(e: MotionEvent): Boolean {resetTransformations()invalidate()return true}override fun onFling(e1: MotionEvent,e2: MotionEvent,velocityX: Float,velocityY: Float): Boolean {scroller.fling(offsetX.toInt(),offsetY.toInt(),velocityX.toInt(),velocityY.toInt(),Int.MIN_VALUE,Int.MAX_VALUE,Int.MIN_VALUE,Int.MAX_VALUE,100,100)invalidate()return true}})scaleDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {isScaling = truepivotX = detector.focusXpivotY = detector.focusYreturn true}override fun onScale(detector: ScaleGestureDetector): Boolean {val scale = detector.scaleFactorval newScale = scaleFactor * scaleif (newScale in minScale..maxScale) {// 調整偏移量保持錨點位置offsetX += (pivotX - offsetX) * (1 - scale)offsetY += (pivotY - offsetY) * (1 - scale)scaleFactor = newScale}applyBoundaryConstraints()invalidate()return true}override fun onScaleEnd(detector: ScaleGestureDetector) {isScaling = false}})rotationDetector = RotationGestureDetector(object : RotationGestureDetector.OnRotationGestureListener {override fun onRotate(angleDelta: Float) {rotationAngle += angleDeltarotationAngle %= 360invalidate()}})}override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)originalRect.set(0f, 0f, w.toFloat(), h.toFloat())resetTransformations()}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)canvas.save()// 應用變換矩陣canvas.translate(offsetX, offsetY)canvas.scale(scaleFactor, scaleFactor, pivotX, pivotY)canvas.rotate(rotationAngle, pivotX, pivotY)// 繪制圓形canvas.drawCircle(originalRect.centerX(),originalRect.centerY(),circleRadius,paint)canvas.restore()}override fun computeScroll() {if (scroller.computeScrollOffset()) {offsetX = scroller.currX.toFloat()offsetY = scroller.currY.toFloat()applyBoundaryConstraints()invalidate()}}override fun onTouchEvent(event: MotionEvent): Boolean {scaleDetector.onTouchEvent(event)gestureDetector.onTouchEvent(event)rotationDetector.onTouchEvent(event)handleMultiTouch(event)return true}private fun handleMultiTouch(event: MotionEvent) {when (event.actionMasked) {MotionEvent.ACTION_POINTER_DOWN -> {activePointerId = event.getPointerId(event.actionIndex)}MotionEvent.ACTION_POINTER_UP -> {val remainingPointer = if (event.actionIndex == 0) 1 else 0activePointerId = event.getPointerId(remainingPointer)}}}private fun applyBoundaryConstraints() {val viewWidth = originalRect.width()val viewHeight = originalRect.height()val scaledWidth = viewWidth * scaleFactorval scaledHeight = viewHeight * scaleFactorval maxOffsetX = (scaledWidth - viewWidth) / 2val maxOffsetY = (scaledHeight - viewHeight) / 2offsetX = offsetX.coerceIn(-maxOffsetX, maxOffsetX)offsetY = offsetY.coerceIn(-maxOffsetY, maxOffsetY)}private fun resetTransformations() {offsetX = 0foffsetY = 0fscaleFactor = 1frotationAngle = 0fpivotX = originalRect.centerX()pivotY = originalRect.centerY()invalidate()}// 自定義旋轉手勢檢測器private class RotationGestureDetector(private val listener: OnRotationGestureListener) {private var prevAngle = 0ffun onTouchEvent(event: MotionEvent): Boolean {if (event.pointerCount != 2) return falsewhen (event.actionMasked) {MotionEvent.ACTION_POINTER_DOWN -> prevAngle = getAngle(event)MotionEvent.ACTION_MOVE -> {val newAngle = getAngle(event)listener.onRotate(newAngle - prevAngle)prevAngle = newAngle}}return true}private fun getAngle(event: MotionEvent): Float {val dx = event.getX(0) - event.getX(1)val dy = event.getY(0) - event.getY(1)return Math.toDegrees(atan2(dy.toDouble(), dx.toDouble())).toFloat()}interface OnRotationGestureListener {fun onRotate(angleDelta: Float)}}
}

功能實現說明

  1. 單指拖動

    • 通過GestureDetector檢測滾動事件
    • onScroll中更新offsetX/Y
    • 添加邊界約束防止移出可視區域
  2. 雙指縮放(帶錨點)

    • 使用ScaleGestureDetector檢測縮放手勢
    • 記錄縮放錨點(pivotX, pivotY)
    • 動態調整偏移量保持錨點位置穩定
    • 限制縮放范圍(0.5-5倍)
  3. 雙指旋轉

    • 自定義RotationGestureDetector計算旋轉角度
    • 通過兩點坐標計算旋轉角度差值
    • 更新rotationAngle并限制在0-360度之間
  4. 慣性滑動

    • 使用OverScroller實現流暢的慣性滑動
    • onFling中初始化滑動參數
    • computeScroll中持續更新位置
  5. 雙擊重置

    • onDoubleTap中重置所有變換參數
    • 重置位置、縮放、旋轉到初始狀態
  6. 邊界約束

    • applyBoundaryConstraints方法計算最大偏移量
    • 根據當前縮放比例動態調整邊界限制
    • 在每次位置變化后調用約束方法
  7. 多指觸控支持

    • 處理ACTION_POINTER_DOWN/UP事件
    • 跟蹤活動指針ID
    • 正確處理多指手勢的切換

使用方式

  1. 在XML布局中添加:
<com.your.package.AdvancedInteractiveViewandroid:layout_width="match_parent"android:layout_height="match_parent"android:background="#F0F0F0"/>
  1. 自定義屬性建議(可選):
<resources><declare-styleable name="AdvancedInteractiveView"><attr name="minScale" format="float" /><attr name="maxScale" format="float" /><attr name="shapeColor" format="color" /></declare-styleable>
</resources>

性能優化建議

  1. 硬件加速
<application android:hardwareAccelerated="true">
  1. 繪制優化
  • 避免在onDraw中創建新對象
  • 使用canvas.saveLayer()替代多次繪制
  • 對于復雜圖形使用Bitmap緩存
  1. 手勢優化
  • 設置合適的手勢檢測閾值
  • 使用ViewConfiguration獲取系統標準值:
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
  1. 內存管理
  • 在onDetachedFromWindow中釋放資源
  • 使用弱引用持有上下文

可根據需要調整以下參數:

  • circleRadius:初始圓形半徑
  • minScale/maxScale:縮放范圍限制
  • 顏色和樣式通過Paint對象自定義
  • 邊界約束計算邏輯調整

實際使用時可擴展以下功能:

  • 添加更多圖形元素
  • 實現手勢沖突解決策略
  • 添加觸摸反饋動畫
  • 支持更多手勢(如長按、快速滑動等)

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/81762.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/81762.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/81762.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Kotlin 協程 (一)

1. Kotlin 協程的核心概念 1.1 協程&#xff08;Coroutine&#xff09; 定義&#xff1a;協程是一種輕量級的執行上下文&#xff0c;可以在任何時候掛起和恢復&#xff0c;而不需要阻塞線程。特點&#xff1a; 比傳統線程更輕量&#xff0c;開銷更小。支持掛起和恢復&#xf…

機器學習 集成學習方法之隨機森林

集成學習方法之隨機森林 1 集成學習2 隨機森林的算法原理2.1 Sklearn API2.2 示例 1 集成學習 機器學習中有一種大類叫集成學習&#xff08;Ensemble Learning&#xff09;&#xff0c;集成學習的基本思想就是將多個分類器組合&#xff0c;從而實現一個預測效果更好的集成分類…

thinkphp6實現統一監聽并記錄所有執行的sql語句除查詢外

創建文件app/middleware/SqlLogger.php <?php namespace app\middleware;use think\facade\Db; use think\facade\Session;class SqlLogger {public function handle($request, \Closure $next){// 監聽所有SQL $request->ip()Db::listen(function($sql, $time) {if (p…

pytorch訓練可視化工具---TensorBoard

一、目的&#xff1a;為什么使用 TensorBoard 調控模型 使用 TensorBoard 可以幫我們&#xff1a; 實時查看 loss / acc 曲線 → 判斷是否過擬合、欠擬合&#xff1b; 對比不同模型或超參數的效果&#xff1b; 可視化模型結構 → 幫助調試模型設計&#xff1b; 查看權重/梯…

機器學習知識自然語言處理入門

一、引言&#xff1a;當文字遇上數學 —— 自然語言的數字化革命 在自然語言處理&#xff08;NLP&#xff09;的世界里&#xff0c;計算機要理解人類語言&#xff0c;首先需要將文字轉化為數學向量。早期的 One-Hot 編碼如同給每個詞語分配一個唯一的 “房間號”&#xff0c;例…

Linux-線程概念和控制

1.Linux線程概念 1.1什么是線程 ? 在?個程序?的?個執?路線就叫做線程&#xff08;thread&#xff09;。更準確的定義是&#xff1a;線程是“?個進程內部 的控制序列” ? ?切進程?少都有?個執?線程 ? 線程在進程內部運?&#xff0c;本質是在進程地址空間內運?…

【氮化鎵】低劑量率對GaN HEMT柵極漏電的影響

2024 年 2 月 22 日,中國科學院新疆理化技術研究所的Li等人在《IEEE ACCESS》期刊發表了題為《Degradation Mechanisms of Gate Leakage in GaN-Based HEMTs at Low Dose Rate Irradiation》的文章,基于實驗分析和 TCAD 仿真,研究了低劑量率輻照下基于 GaN 的 p 型柵高電子遷…

.NET Core 中 Swagger 配置詳解:常用配置與實戰技巧

隨著微服務架構和 RESTful API 的廣泛應用&#xff0c;API 文檔的管理和自動化生成成為了開發中的重要部分。Swagger&#xff08;現為 OpenAPI&#xff09;是一款功能強大的工具&#xff0c;它可以自動生成 API 文檔&#xff0c;并提供交互式 UI&#xff0c;幫助開發者、測試人…

海康工業相機白平衡比選擇器對應的值被重置后,如何恢復原成像

做項目的時候&#xff0c;有時候手抖&#xff0c;一不小心把一個成熟穩定的項目的相機配置&#xff0c;重置了&#xff0c;如何進行恢復呢&#xff0c;在不知道之前配置數據的情況下。 我在做項目的時候&#xff0c;為了讓這個相機成像穩定一點&#xff0c;尤其是做顏色檢測時…

【八股戰神篇】Java虛擬機(JVM)高頻面試題

目錄 專欄簡介 一 請解釋Java虛擬機(JVM)及其主要功能 延伸 1. JVM的基本概念 2. JVM的主要功能 二 對象創建的過程了解嗎 延伸 1.Java 創建對象的四種常見方式 三 什么是雙親委派模型 延伸 1.雙親委派機制的作用: 2.雙親委派模型的核心思想: 3.雙親委派模型的…

win10 上刪除文件夾失敗的一個原因:sqlYog 備份/導出關聯了該文件夾

在嘗試刪除路徑為.../bak/sql的文件時&#xff0c;系統提示無權限操作。然而&#xff0c;關閉SQLyog后&#xff0c;刪除操作成功完成。這表明SQLyog可能正在占用該文件&#xff0c;導致刪除權限受限。關閉SQLyog后&#xff0c;文件被釋放&#xff0c;刪除操作得以順利進行。建議…

Oracle中如何解決LATCH:CACHE BUFFERS LRU CHAIN

簡單來講&#xff0c;Oracle為了高效管理BUFFER CACHE主要使用以下2種LRU列&#xff1a; ?LRU列&#xff0c;又叫替換列&#xff08;replacement list&#xff09;&#xff0c;其中又分為主列和輔助列。 主列&#xff1a;已使用的緩沖區列&#xff0c;分為HOT和COLD區域。HOT區…

C++:迭代器

迭代器的本質&#xff1a;對象。 迭代器與指針類似&#xff0c;通過迭代器可以指向容器中的某個元素&#xff0c;還可以對元素進行操作。 迭代器統一規范了遍歷方式。不同的數據結構可以用統一的方式去遍歷。 接下來是一個自定義迭代器的代碼示例。 #include<iostream&g…

(4)Java虛擬線程與傳統線程對比

虛擬線程與傳統線程對比 &#x1f504; &#x1f4cb; 核心問題 Project Loom的虛擬線程與傳統線程在資源消耗上有何區別&#xff1f;如何設計一個支持百萬級并發的服務&#xff1f; &#x1f4ca; 資源消耗比較 &#x1f418; 傳統線程 &#x1f4cf; 每線程約1MB棧空間&am…

Java 單元測試框架比較:JUnit、TestNG 哪個更適合你?

Java 單元測試框架比較&#xff1a;JUnit、TestNG 哪個更適合你&#xff1f; 在 Java 開發領域&#xff0c;單元測試是保證代碼質量的重要環節。而選擇一個合適的單元測試框架&#xff0c;對于提升測試效率和代碼可靠性至關重要。本文將深入比較 JUnit 和 TestNG 這兩個主流的…

從零開始的抽獎系統創作(2)

我們接著進行抽獎系統的完善。 前面我們完成了 1.結構初始化&#xff08;統一結果返回之類的&#xff0c;還有包的分類&#xff09; 2.加密&#xff08;基于Hutool進行的對稱與非對稱加密&#xff09; 3.用戶注冊 接下來我們先完善一下結構&#xff08;統一異常處理&#…

【vs2022的C#窗體項目】打開運行+sql Server改為mysql數據庫+發布

1. vs2022打開運行原sql Server的C#窗體項目更改為mysql數據庫 1.1. vs2022安裝基礎模塊即可 安裝1??vs核心編輯器2??.net桌面開發必選&#xff0c;可選均不安裝&#xff01;&#xff01;&#xff01; 為了成功連接mysql數據庫&#xff0c;需要安裝組件NuGet包管理器 安…

AI 編程 “幻覺” 風險頻發?飛算 JavaAI 硬核技術筑牢安全防線

AI 技術已深度融入編程領域&#xff0c;為開發者帶來前所未有的便利與效率提升。然而&#xff0c;AI 編程 “幻覺” 問題如影隨形&#xff0c;頻頻引發困擾&#xff0c;成為阻礙行業穩健發展的潛在風險。飛算 JavaAI 憑借一系列硬核技術&#xff0c;強勢出擊&#xff0c;為攻克…

數據庫----軟考中級軟件設計師(自用學習筆記)

目錄 1、E-R圖 2、結構數據模型 3、數據庫的三級模式結構 4、關系代數 5、查詢 6、SQL控制語句 7、視圖?編輯 8、索引 9、關系模式 10、函數依賴 11、通過閉包求候選碼 12、范式 13、無損連接和保持函數依賴 14、數據庫設計 15、數據庫的控制功能 16、數據庫…

【Qt】Qt常見控件的相關知識點

1.close退出槽函數 2.設置快捷鍵&#xff0c;QMenu 。 適用&字母就能設置快捷鍵&#xff0c;運行qt程序&#xff0c;最后就可以按Alt對應的字母進行快捷操作。 3.QMenuBar內存泄露問題 如果ui已經自動生成了menubar&#xff0c;我們再次生成一個新的菜單欄&#xff0c;而…