Procházet zdrojové kódy

[feat]键盘插件,实现剪切板监听和剪切板操作

hezihao před 8 měsíci
rodič
revize
6ff01f81a8
14 změnil soubory, kde provedl 340 přidání a 33 odebrání
  1. 6 3
      plugins/keyboard_android/android/build.gradle
  2. 5 0
      plugins/keyboard_android/android/src/main/AndroidManifest.xml
  3. 46 4
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/child/impl/PasteBarComponent.kt
  4. 38 13
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/keyboard/CustomKeyboardService.kt
  5. 19 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/keyboard/ICustomKeyboardService.kt
  6. 68 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/keyboard/ext/InputMethodLifecycleService.kt
  7. 17 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/mvvm/ViewModelManager.kt
  8. 8 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/mvvm/repository/KeyboardRepository.kt
  9. 24 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/mvvm/viewmodel/KeyboardViewModel.kt
  10. 80 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/ClipboardHelper.kt
  11. 13 10
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/KeyboardHolder.kt
  12. 8 1
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/PermissionDialogUtil.kt
  13. 1 0
      plugins/keyboard_android/android/src/main/res/layout/component_login_page.xml
  14. 7 2
      plugins/keyboard_android/android/src/main/res/layout/component_paste_bar.xml

+ 6 - 3
plugins/keyboard_android/android/build.gradle

@@ -77,6 +77,9 @@ android {
         implementation 'androidx.recyclerview:recyclerview:1.3.0'
         // ConstraintLayout
         implementation "androidx.constraintlayout:constraintlayout:2.1.4"
+        // 让Service支持Jetpack Lifecycle组件
+        implementation "androidx.lifecycle:lifecycle-service:2.6.1"
+
         // Gson
         implementation "com.google.code.gson:gson:2.10"
         // CircleImageView
@@ -102,9 +105,9 @@ android {
             useJUnitPlatform()
 
             testLogging {
-               events "passed", "skipped", "failed", "standardOut", "standardError"
-               outputs.upToDateWhen {false}
-               showStandardStreams = true
+                events "passed", "skipped", "failed", "standardOut", "standardError"
+                outputs.upToDateWhen { false }
+                showStandardStreams = true
             }
         }
     }

+ 5 - 0
plugins/keyboard_android/android/src/main/AndroidManifest.xml

@@ -1,7 +1,12 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     package="com.atmob.keyboard_android">
 
     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
+    <uses-permission
+        android:name="android.permission.READ_CLIPBOARD_IN_BACKGROUND"
+        tools:ignore="ProtectedPermissions" />
+
     <application>
         <meta-data
             android:name="com.bumptech.glide.integration.okhttp3.OkHttpGlideModule"

+ 46 - 4
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/child/impl/PasteBarComponent.kt

@@ -3,10 +3,14 @@ package com.atmob.keyboard_android.component.child.impl
 import android.content.Context
 import android.util.AttributeSet
 import android.view.View
+import android.widget.TextView
 import com.atmob.keyboard_android.R
 import com.atmob.keyboard_android.component.base.BaseUIComponent
 import com.atmob.keyboard_android.component.child.IPasteBarComponent
 import com.atmob.keyboard_android.ext.click
+import com.atmob.keyboard_android.ext.setGone
+import com.atmob.keyboard_android.ext.setVisible
+import com.atmob.keyboard_android.util.KeyboardHolder
 
 /**
  * 粘贴内容栏
@@ -14,19 +18,57 @@ import com.atmob.keyboard_android.ext.click
 class PasteBarComponent @JvmOverloads constructor(
     context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
 ) : BaseUIComponent<IPasteBarComponent>(context, attrs, defStyleAttr), IPasteBarComponent {
-    private lateinit var vClearBtn: View
+    private lateinit var vTipSymbol: View
+    private lateinit var vTip: TextView
+    private lateinit var vClear: View
 
     override fun onInflateViewId(): Int {
         return R.layout.component_paste_bar
     }
 
     override fun findView(view: View) {
-        vClearBtn = view.findViewById(R.id.clear_btn)
+        vTipSymbol = view.findViewById(R.id.tip_symbol)
+        vTip = view.findViewById(R.id.tip)
+        vClear = view.findViewById(R.id.clear)
     }
 
     override fun bindView(view: View) {
-        vClearBtn.click {
-            // 清空输入框内容
+        vClear.click {
+            // 清空用户粘贴的内容
+            val keyboardService = KeyboardHolder.getKeyboardService()
+            keyboardService?.getKeyboardViewModel()?.let {
+                it.updateUserClipboardData("")
+            }
+        }
+        setData()
+    }
+
+    private fun setData() {
+        // 监听用户的剪切板复制内容
+        val keyboardService = KeyboardHolder.getKeyboardService()
+        keyboardService?.getKeyboardViewModel()?.let {
+            it.userClipboardData.observeForever { userClipboardData ->
+                render(userClipboardData)
+            }
+        }
+    }
+
+    /**
+     * 渲染
+     *
+     * @param userClipboardData 用户的剪切板数据
+     */
+    private fun render(userClipboardData: String) {
+        // 没有复制内容,显示前面的标识图标和提示文案,隐藏清除按钮
+        if (userClipboardData.isBlank()) {
+            vTipSymbol.setVisible()
+            vTip.text = context.resources.getString(R.string.paste_tip)
+            vClear.setGone()
+        } else {
+            // 有复制内容,显示用户复制的内容和清除内容图标,隐藏前面的标识图标
+            vTipSymbol.setGone()
+            vTip.text = userClipboardData
+            vClear.setVisible()
         }
     }
 

+ 38 - 13
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/keyboard/CustomKeyboardService.kt

@@ -1,14 +1,18 @@
 package com.atmob.keyboard_android.keyboard
 
 import android.annotation.SuppressLint
-import android.inputmethodservice.InputMethodService
 import android.view.View
+import android.view.Window
 import android.view.inputmethod.EditorInfo
 import android.widget.Button
 import android.widget.EditText
 import android.widget.GridLayout
 import android.widget.Toast
 import com.atmob.keyboard_android.R
+import com.atmob.keyboard_android.keyboard.ext.InputMethodLifecycleService
+import com.atmob.keyboard_android.mvvm.ViewModelManager
+import com.atmob.keyboard_android.mvvm.viewmodel.KeyboardViewModel
+import com.atmob.keyboard_android.util.ClipboardHelper
 import com.atmob.keyboard_android.util.InputMethodUtil
 import com.atmob.keyboard_android.util.KeyboardHolder
 import com.atmob.keyboard_android.util.LogUtil
@@ -19,7 +23,8 @@ import io.flutter.plugin.common.MethodChannel
 /**
  * 自定义键盘的输入法服务
  */
-class CustomKeyboardService : InputMethodService() {
+class CustomKeyboardService : InputMethodLifecycleService(), ICustomKeyboardService,
+    ClipboardHelper.OnUserClipboardDataUpdateListener {
     /**
      * 用于与 Flutter 端通信的 MethodChannel
      */
@@ -35,9 +40,22 @@ class CustomKeyboardService : InputMethodService() {
      */
     private var mKeyMappings: List<Pair<String, String>> = listOf()
 
+    /**
+     * 键盘ViewModel
+     */
+    private val mKeyboardViewModel by lazy {
+        ViewModelManager.getKeyboardViewModel(this@CustomKeyboardService)
+    }
+
+    override fun onUserClipboardDataUpdate(newText: String) {
+        // 剪切板数据更新
+        mKeyboardViewModel.updateUserClipboardData(newText)
+    }
+
     override fun onCreate() {
         super.onCreate()
-        LogUtil.d("输入法服务已启动!")
+        // 保存输入法Service的实例
+        KeyboardHolder.attachKeyboardService(this)
 
         val flutterEngine = FlutterEngineCache.getInstance().get("my_engine_id")
         if (flutterEngine != null) {
@@ -63,10 +81,12 @@ class CustomKeyboardService : InputMethodService() {
         } else {
             LogUtil.e("FlutterEngine 未找到,MethodChannel 无法初始化")
         }
+
+        // 监听用户的剪切板
+        ClipboardHelper.registerClipboardListener(this)
     }
 
     override fun onCreateInputView(): View {
-        LogUtil.d("onCreateInputView!")
         val keyboardView = layoutInflater.inflate(R.layout.keyboard_layout, null)
         vKeyboardView = keyboardView
 
@@ -76,21 +96,18 @@ class CustomKeyboardService : InputMethodService() {
         return keyboardView
     }
 
-    override fun onDestroy() {
-        super.onDestroy()
-        KeyboardHolder.detachKeyboardWindow()
-    }
-
     override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
         super.onStartInputView(info, restarting)
-        LogUtil.d("onStartInputView: 重新加载键盘数据")
-
-        KeyboardHolder.attachKeyboardWindow(window!!.window!!)
-
         // 重新获取按键映射
         fetchKeyMappings()
     }
 
+    override fun onDestroy() {
+        super.onDestroy()
+        KeyboardHolder.detachKeyboardService()
+        ClipboardHelper.unRegisterClipboardListener(this)
+    }
+
     /**
      * 通过 KeyboardAndroidPlugin 获取按键映射
      */
@@ -197,4 +214,12 @@ class CustomKeyboardService : InputMethodService() {
                 }
             })
     }
+
+    override fun getKeyboardWindow(): Window {
+        return window!!.window!!
+    }
+
+    override fun getKeyboardViewModel(): KeyboardViewModel {
+        return mKeyboardViewModel
+    }
 }

+ 19 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/keyboard/ICustomKeyboardService.kt

@@ -0,0 +1,19 @@
+package com.atmob.keyboard_android.keyboard
+
+import android.view.Window
+import com.atmob.keyboard_android.mvvm.viewmodel.KeyboardViewModel
+
+/**
+ * 自定义键盘Service接口,定义对外暴露的API方法
+ */
+interface ICustomKeyboardService {
+    /**
+     * 获取自定义键盘的Window
+     */
+    fun getKeyboardWindow(): Window
+
+    /**
+     * 获取键盘ViewModel
+     */
+    fun getKeyboardViewModel(): KeyboardViewModel
+}

+ 68 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/keyboard/ext/InputMethodLifecycleService.kt

@@ -0,0 +1,68 @@
+package com.atmob.keyboard_android.keyboard.ext
+
+import android.content.Intent
+import android.inputmethodservice.InputMethodService
+import androidx.annotation.CallSuper
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ServiceLifecycleDispatcher
+import androidx.lifecycle.ViewModelStore
+import androidx.lifecycle.ViewModelStoreOwner
+
+/**
+ * 拓展InputMethodService,支持Jetpack组件的Lifecycle生命周期组件,参考自LifecycleService,并同时实现了ViewModelStoreOwner,支持使用ViewModel
+ */
+open class InputMethodLifecycleService() :
+    InputMethodService(), LifecycleOwner, ViewModelStoreOwner {
+    private val dispatcher = ServiceLifecycleDispatcher(this)
+
+    // 实现 ViewModelStoreOwner
+    override val viewModelStore: ViewModelStore
+        get() = ViewModelStore()
+
+    override val lifecycle: Lifecycle
+        get() = dispatcher.lifecycle
+
+    @CallSuper
+    override fun onCreate() {
+        dispatcher.onServicePreSuperOnCreate()
+        super.onCreate()
+    }
+
+    // onBind()方法,被InputMethodService类,修饰为final,无法被重写,所以只能重写该方法来代替
+    override fun onCreateInputMethodInterface(): AbstractInputMethodImpl? {
+        dispatcher.onServicePreSuperOnBind()
+        return super.onCreateInputMethodInterface()
+    }
+
+//    @CallSuper
+//    override fun onBind(intent: Intent): IBinder? {
+//        dispatcher.onServicePreSuperOnBind()
+//        return null
+//    }
+
+    @Deprecated("Deprecated in Java")
+    @Suppress("DEPRECATION")
+    @CallSuper
+    override fun onStart(intent: Intent?, startId: Int) {
+        dispatcher.onServicePreSuperOnStart()
+        super.onStart(intent, startId)
+    }
+
+    // this method is added only to annotate it with @CallSuper.
+    // In usual Service, super.onStartCommand is no-op, but in LifecycleService
+    // it results in dispatcher.onServicePreSuperOnStart() call, because
+    // super.onStartCommand calls onStart().
+    @CallSuper
+    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        return super.onStartCommand(intent, flags, startId)
+    }
+
+    @CallSuper
+    override fun onDestroy() {
+        dispatcher.onServicePreSuperOnDestroy()
+        // 清理ViewModel
+        viewModelStore.clear()
+        super.onDestroy()
+    }
+}

+ 17 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/mvvm/ViewModelManager.kt

@@ -0,0 +1,17 @@
+package com.atmob.keyboard_android.mvvm
+
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelStoreOwner
+import com.atmob.keyboard_android.mvvm.viewmodel.KeyboardViewModel
+
+/**
+ * ViewModel管理器
+ */
+object ViewModelManager {
+    /**
+     * 获取键盘ViewModel
+     */
+    fun getKeyboardViewModel(owner: ViewModelStoreOwner): KeyboardViewModel {
+        return ViewModelProvider(owner)[KeyboardViewModel::class.java]
+    }
+}

+ 8 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/mvvm/repository/KeyboardRepository.kt

@@ -0,0 +1,8 @@
+package com.atmob.keyboard_android.mvvm.repository
+
+/**
+ * 键盘Repository
+ */
+object KeyboardRepository {
+
+}

+ 24 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/mvvm/viewmodel/KeyboardViewModel.kt

@@ -0,0 +1,24 @@
+package com.atmob.keyboard_android.mvvm.viewmodel
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+/**
+ * 键盘ViewModel
+ */
+class KeyboardViewModel : ViewModel() {
+    /**
+     * 用户的剪切板数据
+     */
+    private val _userClipboardData = MutableLiveData<String>("")
+
+    val userClipboardData: LiveData<String> = _userClipboardData
+
+    /**
+     * 更新用户的剪切板数据
+     */
+    fun updateUserClipboardData(newText: String) {
+        _userClipboardData.value = newText
+    }
+}

+ 80 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/ClipboardHelper.kt

@@ -0,0 +1,80 @@
+package com.atmob.keyboard_android.util
+
+import android.annotation.SuppressLint
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.ClipboardManager.OnPrimaryClipChangedListener
+import android.content.Context
+import android.content.Context.CLIPBOARD_SERVICE
+
+/**
+ * 剪切板工具类
+ */
+@SuppressLint("StaticFieldLeak")
+object ClipboardHelper : OnPrimaryClipChangedListener {
+    private val mContext = ContextUtil.getContext().applicationContext
+
+    /**
+     * 监听器列表
+     */
+    private val mListenerList = mutableListOf<OnUserClipboardDataUpdateListener>()
+
+    interface OnUserClipboardDataUpdateListener {
+        fun onUserClipboardDataUpdate(newText: String)
+    }
+
+    init {
+        val clipboardManager = getClipboardManager(mContext)
+        clipboardManager.addPrimaryClipChangedListener(this)
+    }
+
+    override fun onPrimaryClipChanged() {
+        for (listener in mListenerList) {
+            listener.onUserClipboardDataUpdate(getUserClipboardData())
+        }
+    }
+
+    /**
+     * 注册用户剪切板的数据监听
+     */
+    fun registerClipboardListener(listener: OnUserClipboardDataUpdateListener) {
+        if (!mListenerList.contains(listener)) {
+            mListenerList.add(listener)
+        }
+    }
+
+    /**
+     * 取消注册用户剪切板的数据监听
+     */
+    fun unRegisterClipboardListener(listener: OnUserClipboardDataUpdateListener) {
+        mListenerList.remove(listener)
+    }
+
+    /**
+     * 获取用户剪切板的数据
+     */
+    fun getUserClipboardData(): String {
+        val clipboardManager = getClipboardManager(mContext)
+        val clip = clipboardManager.primaryClip
+        // 处理剪切板内容
+        if (clip != null && clip.itemCount > 0) {
+            return clip.getItemAt(0).text.toString()
+        }
+        return ""
+    }
+
+    /**
+     * 清空用户剪切板
+     */
+    fun clearClipboardData() {
+        val clipboardManager = getClipboardManager(mContext)
+        clipboardManager.setPrimaryClip(ClipData.newPlainText(null, ""))
+    }
+
+    /**
+     * 获取剪切板管理器
+     */
+    private fun getClipboardManager(context: Context): ClipboardManager {
+        return context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
+    }
+}

+ 13 - 10
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/KeyboardHolder.kt

@@ -1,30 +1,33 @@
 package com.atmob.keyboard_android.util
 
 import android.annotation.SuppressLint
-import android.view.Window
+import com.atmob.keyboard_android.keyboard.ICustomKeyboardService
 
 @SuppressLint("StaticFieldLeak")
 object KeyboardHolder {
-    private var keyboardWindow: Window? = null
+    /**
+     * 软键盘Service
+     */
+    private var mCustomKeyboardService: ICustomKeyboardService? = null
 
     /**
-     * 保存软键盘的Window
+     * 保存软键盘Service实例
      */
-    fun attachKeyboardWindow(window: Window) {
-        this.keyboardWindow = window
+    fun attachKeyboardService(service: ICustomKeyboardService) {
+        this.mCustomKeyboardService = service
     }
 
     /**
      * 获取软键盘的Window
      */
-    fun getKeyboardWindow(): Window? {
-        return this.keyboardWindow
+    fun getKeyboardService(): ICustomKeyboardService? {
+        return this.mCustomKeyboardService
     }
 
     /**
-     * 移除软键盘的Window
+     * 移除软键盘的Service实例
      */
-    fun detachKeyboardWindow() {
-        this.keyboardWindow = null
+    fun detachKeyboardService() {
+        this.mCustomKeyboardService = null
     }
 }

+ 8 - 1
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/PermissionDialogUtil.kt

@@ -14,9 +14,16 @@ class PermissionDialogUtil {
          * 显示权限弹窗
          */
         fun showPermissionDialog(context: Context) {
+            val keyboardService = KeyboardHolder.getKeyboardService()
+            if (keyboardService == null) {
+                return
+            }
+
+            val keyboardWindow = keyboardService.getKeyboardWindow()
+
             ConfirmDialog(context)
                 .apply {
-                    setServiceWindowToken(KeyboardHolder.getKeyboardWindow()!!)
+                    setServiceWindowToken(keyboardWindow)
                 }
                 .bindDataModel(
                     ConfirmDialog.DataModel(

+ 1 - 0
plugins/keyboard_android/android/src/main/res/layout/component_login_page.xml

@@ -35,6 +35,7 @@
         android:text="@string/login_tip"
         android:textColor="@color/text_color_primary"
         android:textSize="20sp"
+        android:textStyle="bold"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@+id/app_icon" />

+ 7 - 2
plugins/keyboard_android/android/src/main/res/layout/component_paste_bar.xml

@@ -3,13 +3,16 @@
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:background="@drawable/bg_paste_bar"
+    android:paddingStart="10dp"
     android:paddingTop="15dp"
+    android:paddingEnd="10dp"
     android:paddingBottom="15dp">
 
     <LinearLayout
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_centerInParent="true"
+        android:layout_marginEnd="40dp"
         android:gravity="center_vertical"
         android:orientation="horizontal">
 
@@ -26,6 +29,9 @@
             android:id="@+id/tip"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:ellipsize="end"
+            android:maxLines="1"
             android:text="@string/paste_tip"
             android:textColor="@color/text_paste_tip"
             android:textSize="14sp"
@@ -33,11 +39,10 @@
     </LinearLayout>
 
     <ImageView
-        android:id="@+id/clear_btn"
+        android:id="@+id/clear"
         android:layout_width="16dp"
         android:layout_height="16dp"
         android:layout_alignParentEnd="true"
         android:layout_centerVertical="true"
-        android:layout_marginEnd="10dp"
         android:src="@mipmap/ic_clear_input" />
 </RelativeLayout>