Browse Source

[feat]键盘插件,实现键盘选择页UI

hezihao 1 year ago
parent
commit
4e5b641550
36 changed files with 1893 additions and 15 deletions
  1. 8 0
      plugins/keyboard_android/android/build.gradle
  2. 10 5
      plugins/keyboard_android/android/src/main/AndroidManifest.xml
  3. 110 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/KeyboardSelectComponent.kt
  4. 0 4
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/base/BaseUIComponent.kt
  5. 0 5
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/base/IUIComponent.kt
  6. 36 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/item/EmptyPlaceholderViewBinder.kt
  7. 64 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/item/KeyboardSelectViewBinder.kt
  8. 13 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/constant/Constants.kt
  9. 365 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/ext/ViewExt.kt
  10. 11 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/model/EmptyPlaceholderModel.kt
  11. 20 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/model/KeyboardSelectModel.kt
  12. 18 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/imageloader/ImageLoadProgressListener.java
  13. 70 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/imageloader/ImageLoader.java
  14. 20 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/imageloader/LoadImageCallback.java
  15. 222 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/imageloader/LoadOption.java
  16. 38 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/imageloader/progress/ProgressInterceptor.java
  17. 11 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/imageloader/progress/ProgressListener.java
  18. 72 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/imageloader/progress/ProgressResponseBody.java
  19. 76 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/imageloader/strategy/ILoaderStrategy.java
  20. 264 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/imageloader/strategy/impl/GlideLoader.java
  21. 48 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/listener/DelayOnClickListener.java
  22. 47 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/recyclerview/GridDivider.kt
  23. 44 0
      plugins/keyboard_android/android/src/main/kotlin/com/bumptech/glide/integration/okhttp3/OkHttpGlideModule.java
  24. 30 0
      plugins/keyboard_android/android/src/main/kotlin/com/bumptech/glide/integration/okhttp3/OkHttpLibraryGlideModule.java
  25. 115 0
      plugins/keyboard_android/android/src/main/kotlin/com/bumptech/glide/integration/okhttp3/OkHttpStreamFetcher.java
  26. 88 0
      plugins/keyboard_android/android/src/main/kotlin/com/bumptech/glide/integration/okhttp3/OkHttpUrlLoader.java
  27. 7 0
      plugins/keyboard_android/android/src/main/res/drawable/bg_keyboard_normal.xml
  28. 11 0
      plugins/keyboard_android/android/src/main/res/drawable/bg_keyboard_selected.xml
  29. 9 1
      plugins/keyboard_android/android/src/main/res/layout/component_key_board_container.xml
  30. 27 0
      plugins/keyboard_android/android/src/main/res/layout/component_keyboard_select.xml
  31. 5 0
      plugins/keyboard_android/android/src/main/res/layout/item_empty_placeholder.xml
  32. 31 0
      plugins/keyboard_android/android/src/main/res/layout/item_keyboard_select.xml
  33. BIN
      plugins/keyboard_android/android/src/main/res/mipmap-xxxhdpi/ic_common_keyboard_icon.png
  34. BIN
      plugins/keyboard_android/android/src/main/res/mipmap-xxxhdpi/ic_keyboard_default_icon.png
  35. 2 0
      plugins/keyboard_android/android/src/main/res/values/dimens.xml
  36. 1 0
      plugins/keyboard_android/android/src/main/res/values/string.xml

+ 8 - 0
plugins/keyboard_android/android/build.gradle

@@ -62,6 +62,8 @@ android {
         minSdk = 21
     }
 
+    def glide_version = '4.13.2'
+
     dependencies {
         testImplementation("org.jetbrains.kotlin:kotlin-test")
         testImplementation("org.mockito:mockito-core:5.0.0")
@@ -85,6 +87,12 @@ android {
         implementation 'com.blankj:utilcodex:1.31.1'
         // RecyclerView Adapter
         implementation 'me.drakeet.multitype:multitype:3.5.0'
+        // OkHttp3
+        implementation 'com.squareup.okhttp3:okhttp:3.12.0'
+        // Glide
+        implementation "com.github.bumptech.glide:glide:$glide_version"
+        implementation "jp.wasabeef:glide-transformations:4.3.0"
+        implementation "com.github.zjupure:webpdecoder:2.1.${glide_version}"
     }
 
     testOptions {

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

@@ -3,15 +3,20 @@
 
     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
     <application>
-        <activity android:name=".keyboard.InputMethodPickerActivity"
-            android:theme="@android:style/Theme.Translucent.NoTitleBar"
-            android:launchMode="singleTask"/>
+        <meta-data
+            android:name="com.bumptech.glide.integration.okhttp3.OkHttpGlideModule"
+            android:value="GlideModule" />
+
+        <activity
+            android:name=".keyboard.InputMethodPickerActivity"
+            android:launchMode="singleTask"
+            android:theme="@android:style/Theme.Translucent.NoTitleBar" />
 
         <service
             android:name=".keyboard.CustomKeyboardService"
             android:exported="true"
-            android:permission="android.permission.BIND_INPUT_METHOD"
-            android:label="追爱小键盘">
+            android:label="追爱小键盘"
+            android:permission="android.permission.BIND_INPUT_METHOD">
             <intent-filter>
                 <action android:name="android.view.InputMethod" />
             </intent-filter>

+ 110 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/KeyboardSelectComponent.kt

@@ -0,0 +1,110 @@
+package com.atmob.keyboard_android.component
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.atmob.keyboard_android.R
+import com.atmob.keyboard_android.component.base.BaseUIComponent
+import com.atmob.keyboard_android.component.item.EmptyPlaceholderViewBinder
+import com.atmob.keyboard_android.component.item.KeyboardSelectViewBinder
+import com.atmob.keyboard_android.constant.Constants
+import com.atmob.keyboard_android.ext.click
+import com.atmob.keyboard_android.model.EmptyPlaceholderModel
+import com.atmob.keyboard_android.model.KeyboardSelectModel
+import com.atmob.keyboard_android.util.recyclerview.GridDivider
+import com.blankj.utilcode.util.ConvertUtils
+import me.drakeet.multitype.Items
+import me.drakeet.multitype.MultiTypeAdapter
+
+/**
+ * 键盘选择页
+ */
+class KeyboardSelectComponent @JvmOverloads constructor(
+    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : BaseUIComponent(context, attrs, defStyleAttr) {
+    private lateinit var vBackBtn: View
+    private lateinit var vList: RecyclerView
+
+    private lateinit var mListItems: Items
+    private lateinit var mListAdapter: MultiTypeAdapter
+
+    override fun onInflateViewId(): Int {
+        return R.layout.component_keyboard_select
+    }
+
+    override fun findView(view: View) {
+        vBackBtn = view.findViewById(R.id.back_btn)
+        vList = view.findViewById(R.id.list)
+    }
+
+    @SuppressLint("NotifyDataSetChanged")
+    override fun bindView(view: View) {
+        vBackBtn.click {
+            // 关闭选择页
+        }
+        mListItems = Items()
+        mListAdapter = MultiTypeAdapter(mListItems).apply {
+            // 空占位条目
+            register(EmptyPlaceholderModel::class.java, EmptyPlaceholderViewBinder())
+            // 键盘条目
+            register(KeyboardSelectModel::class.java, KeyboardSelectViewBinder { item ->
+                // 先全部取消选中,再选中当前设置的键盘
+                mListItems.forEachIndexed { index, item ->
+                    if (item is KeyboardSelectModel) {
+                        item.isSelected = false
+                    }
+                }
+                val targetPosition = mListItems.indexOf(item)
+                val targetItem = mListItems[targetPosition] as KeyboardSelectModel
+                targetItem.isSelected = true
+                notifyDataSetChanged()
+            })
+        }
+        vList.apply {
+            layoutManager = GridLayoutManager(context, Constants.KEYBOARD_SELECT_SPAN_COUNT)
+            adapter = mListAdapter
+            addItemDecoration(
+                GridDivider(
+                    spanCount = Constants.KEYBOARD_SELECT_SPAN_COUNT,
+                    spacing = ConvertUtils.dp2px(9f)
+                )
+            )
+        }
+
+        setData()
+    }
+
+    @SuppressLint("NotifyDataSetChanged")
+    private fun setData() {
+        // TODO: 加载键盘列表
+        val emptyPlaceholderItemHeight = ConvertUtils.dp2px(60f)
+        mListItems.apply {
+            // 添加空占位条目
+            add(EmptyPlaceholderModel(emptyPlaceholderItemHeight))
+            add(EmptyPlaceholderModel(emptyPlaceholderItemHeight))
+            add(EmptyPlaceholderModel(emptyPlaceholderItemHeight))
+            // 添加键盘条目
+            add(
+                KeyboardSelectModel(
+                    "",
+                    "通用键盘",
+                    isSelected = true,
+                    iconDefaultResId = R.mipmap.ic_common_keyboard_icon,
+                    isCommonKeyboard = true
+                )
+            )
+            add(KeyboardSelectModel("", "小蕊"))
+            add(KeyboardSelectModel("", "小胡"))
+            add(KeyboardSelectModel("", "自己&小蕊"))
+            add(KeyboardSelectModel("", "迪丽热巴"))
+            add(KeyboardSelectModel("", "哇咔咔"))
+            add(KeyboardSelectModel("", "小明"))
+            add(KeyboardSelectModel("", "小红"))
+            add(KeyboardSelectModel("", "龙哥"))
+        }
+        mListAdapter.notifyDataSetChanged()
+    }
+}

+ 0 - 4
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/base/BaseUIComponent.kt

@@ -16,10 +16,6 @@ abstract class BaseUIComponent @JvmOverloads constructor(
         val view = LayoutInflater.from(context).inflate(onInflateViewId(), this, true)
         findView(view)
         bindView(view)
-        setData()
-    }
-
-    override fun setData() {
     }
 
     override fun asView(): ViewGroup {

+ 0 - 5
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/base/IUIComponent.kt

@@ -23,11 +23,6 @@ interface IUIComponent {
     fun bindView(view: View)
 
     /**
-     * 设置数据
-     */
-    fun setData()
-
-    /**
      * 将组件转换为View实例
      */
     fun asView(): ViewGroup

+ 36 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/item/EmptyPlaceholderViewBinder.kt

@@ -0,0 +1,36 @@
+package com.atmob.keyboard_android.component.item
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.atmob.keyboard_android.R
+import com.atmob.keyboard_android.model.EmptyPlaceholderModel
+import me.drakeet.multitype.ItemViewBinder
+
+/**
+ * 空占位条目
+ */
+class EmptyPlaceholderViewBinder :
+    ItemViewBinder<EmptyPlaceholderModel, EmptyPlaceholderViewBinder.InnerViewHolder>() {
+    override fun onCreateViewHolder(
+        inflater: LayoutInflater,
+        parent: ViewGroup
+    ): InnerViewHolder {
+        return InnerViewHolder(inflater.inflate(R.layout.item_empty_placeholder, parent, false))
+    }
+
+    override fun onBindViewHolder(
+        holder: InnerViewHolder,
+        item: EmptyPlaceholderModel
+    ) {
+        holder.vItemPlaceholder.apply {
+            layoutParams.height = item.height
+            requestLayout()
+        }
+    }
+
+    inner class InnerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+        val vItemPlaceholder: View = itemView.findViewById(R.id.item_placeholder)
+    }
+}

+ 64 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/component/item/KeyboardSelectViewBinder.kt

@@ -0,0 +1,64 @@
+package com.atmob.keyboard_android.component.item
+
+import android.graphics.Typeface
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.atmob.keyboard_android.R
+import com.atmob.keyboard_android.ext.click
+import com.atmob.keyboard_android.ext.loadUrlImage
+import com.atmob.keyboard_android.model.KeyboardSelectModel
+import me.drakeet.multitype.ItemViewBinder
+
+/**
+ * 键盘选择条目
+ */
+class KeyboardSelectViewBinder(
+    private val onItemClick: (item: KeyboardSelectModel) -> Unit
+) :
+    ItemViewBinder<KeyboardSelectModel, KeyboardSelectViewBinder.InnerViewHolder>() {
+    override fun onCreateViewHolder(
+        inflater: LayoutInflater,
+        parent: ViewGroup
+    ): InnerViewHolder {
+        return InnerViewHolder(inflater.inflate(R.layout.item_keyboard_select, parent, false))
+    }
+
+    override fun onBindViewHolder(
+        holder: InnerViewHolder,
+        item: KeyboardSelectModel
+    ) {
+        val context = holder.itemView.context
+        holder.vIcon.loadUrlImage(item.icon, item.iconDefaultResId)
+        holder.vName.text = item.name
+
+        // 选中
+        if (item.isSelected) {
+            holder.itemContainer.setBackgroundResource(R.drawable.bg_keyboard_selected)
+            holder.vName.apply {
+                setTextColor(context.resources.getColor(R.color.text_color_white))
+                setTypeface(typeface, Typeface.BOLD)
+            }
+        } else {
+            // 未选中
+            holder.itemContainer.setBackgroundResource(R.drawable.bg_keyboard_normal)
+            holder.vName.apply {
+                setTextColor(context.resources.getColor(R.color.text_color_primary))
+                setTypeface(typeface, Typeface.NORMAL)
+            }
+        }
+
+        holder.itemView.click {
+            onItemClick.invoke(item)
+        }
+    }
+
+    inner class InnerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+        val itemContainer: ViewGroup = itemView.findViewById(R.id.item_container)
+        val vIcon: ImageView = itemView.findViewById(R.id.icon)
+        val vName: TextView = itemView.findViewById(R.id.name)
+    }
+}

+ 13 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/constant/Constants.kt

@@ -0,0 +1,13 @@
+package com.atmob.keyboard_android.constant
+
+/**
+ * 常量
+ */
+interface Constants {
+    companion object {
+        /**
+         * 键盘选择,网格列数
+         */
+        val KEYBOARD_SELECT_SPAN_COUNT: Int = 3
+    }
+}

+ 365 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/ext/ViewExt.kt

@@ -0,0 +1,365 @@
+package com.atmob.keyboard_android.ext
+
+import android.app.Activity
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.view.ContextThemeWrapper
+import android.view.View
+import android.view.ViewGroup
+import android.widget.EditText
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import com.atmob.keyboard_android.util.imageloader.ImageLoader
+import com.atmob.keyboard_android.util.imageloader.LoadOption
+import com.atmob.keyboard_android.util.listener.DelayOnClickListener
+
+/**
+ * 测量View
+ */
+fun measureView(target: View): Pair<Int, Int> {
+    val w = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+    val h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+    target.measure(w, h)
+    val width = target.measuredWidth
+    val height = target.measuredHeight
+    return Pair(width, height)
+}
+
+/**
+ * 给View设置带有防暴击的监听
+ */
+fun View.click(listener: (view: View) -> Unit): View {
+    this.setOnClickListener(DelayOnClickListener(listener))
+    return this
+}
+
+/**
+ * 长按
+ */
+fun View.longClick(listener: (view: View) -> Boolean): View {
+    this.setOnLongClickListener {
+        listener(it)
+    }
+    return this
+}
+
+fun <T : View> Fragment.findView(id: Int): T {
+    this.view ?: throw NullPointerException("Fragment view must be not null")
+    return this.requireView().findViewById(id)
+}
+
+fun View.setVisible() {
+    this.visibility = View.VISIBLE
+}
+
+fun View.setGone() {
+    this.visibility = View.GONE
+}
+
+fun View.setInVisible() {
+    this.visibility = View.INVISIBLE
+}
+
+fun View.isHide(): Boolean {
+    return this.visibility != View.VISIBLE
+}
+
+fun TextView.setTextWithDefault(text: CharSequence?, default: CharSequence = "") {
+    if (text == null) {
+        this.text = default
+    } else {
+        this.text = text
+    }
+}
+
+/**
+ * 设置文字,并且将光标移动到末尾
+ */
+fun EditText.setTextWithSelection(text: CharSequence?) {
+    if (text.isNullOrBlank()) {
+        setText("")
+    } else {
+        setText(text)
+        setSelection(text.length)
+    }
+    requestFocus()
+}
+
+/**
+ * 给TextView设置text时去掉null字样
+ */
+var TextView.notNullText: String?
+    get() = text.toString()
+    set(value) {
+        text = value?.replace("null", "") ?: ""
+    }
+
+/**
+ * 判断ViewGroup是否有子View
+ */
+fun ViewGroup.hasChildView(): Boolean {
+    return childCount > 0
+}
+
+/**
+ * 获取ViewGroup所有的子View集合
+ */
+fun ViewGroup.getAllChildView(): MutableList<View> {
+    return mutableListOf<View>().apply {
+        val count = childCount
+        for (viewIndex in 0 until count) {
+            add(getChildAt(viewIndex))
+        }
+    }
+}
+
+/**
+ * 获取ViewGroup的第一个子View
+ */
+val ViewGroup.getFirstChildView: View?
+    get() {
+        return if (childCount > 0) {
+            getChildAt(0)
+        } else {
+            null
+        }
+    }
+
+/**
+ * 获取最后一个子View
+ */
+val ViewGroup.getLastChildView: View?
+    get() {
+        return if (childCount > 0) {
+            getChildAt(this.childCount - 1)
+        } else {
+            null
+        }
+    }
+
+/**
+ * 更新MarginLeft
+ */
+fun ViewGroup.MarginLayoutParams.setMarginLeft(newLeft: Int) {
+    setMargins(newLeft, topMargin, rightMargin, bottomMargin)
+}
+
+/**
+ * 更新MarginRight
+ */
+fun ViewGroup.MarginLayoutParams.setMarginRight(newRight: Int) {
+    setMargins(leftMargin, topMargin, newRight, bottomMargin)
+}
+
+/**
+ * 更新MarginTop
+ */
+fun ViewGroup.MarginLayoutParams.setMarginTop(newTop: Int) {
+    setMargins(leftMargin, newTop, rightMargin, bottomMargin)
+}
+
+/**
+ * 更新MarginBottom
+ */
+fun ViewGroup.MarginLayoutParams.setMarginBottom(newBottom: Int) {
+    setMargins(leftMargin, topMargin, rightMargin, newBottom)
+}
+
+/**
+ * 更新PaddingTop
+ */
+fun View.setPaddingTop(newTop: Int) {
+    setPadding(paddingLeft, newTop, paddingRight, paddingBottom)
+}
+
+/**
+ * 更新PaddingTop
+ */
+fun View.setPaddingLeft(newLeft: Int) {
+    setPadding(newLeft, paddingTop, paddingRight, paddingBottom)
+}
+
+/**
+ * 更新PaddingRight
+ */
+fun View.setPaddingRight(newRight: Int) {
+    setPadding(paddingLeft, paddingTop, newRight, paddingBottom)
+}
+
+/**
+ * 更新PaddingBottom
+ */
+fun View.setPaddingBottom(newBottom: Int) {
+    setPadding(paddingLeft, paddingTop, paddingRight, newBottom)
+}
+
+/**
+ * 获取ImageView上的图像
+ */
+@SuppressWarnings
+fun ImageView.getImageBitmap(): Bitmap? {
+    this.isDrawingCacheEnabled = true
+    val bitmap = this.drawingCache
+    this.isDrawingCacheEnabled = false
+    return bitmap
+}
+
+fun Fragment.setStatusBarBlack() {
+    activity?.run {
+        setStatusBarBlack()
+    }
+}
+
+/**
+ * 获取View的Activity
+ */
+fun View.getActivity(): Activity? {
+    val context: Context = context
+    if (context is Activity) {
+        return context
+    } else if (context is ContextThemeWrapper) {
+        if (context.baseContext is Activity) {
+            return context.baseContext as Activity
+        }
+    }
+    return null
+}
+
+/**
+ * 移除所有CompoundDrawables
+ */
+fun TextView.removeAllCompoundDrawables() {
+    setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
+}
+
+fun TextView.setDrawableLeft(drawableResId: Int) {
+    setDrawableLeft(context.resources.getDrawable(drawableResId)!!)
+}
+
+/**
+ * 动态设置TextView的DrawableLeft
+ */
+fun TextView.setDrawableLeft(drawable: Drawable) {
+    drawable.setBounds(0, 0, drawable.minimumWidth, drawable.minimumHeight)
+    val originCompoundDrawables = compoundDrawables
+    val drawableTop = originCompoundDrawables[1].apply {
+        setBounds()
+    }
+    val drawableRight = originCompoundDrawables[2].apply {
+        setBounds()
+    }
+    val drawableBottom = originCompoundDrawables[3].apply {
+        setBounds()
+    }
+    setCompoundDrawablesWithIntrinsicBounds(drawable, drawableTop, drawableRight, drawableBottom)
+}
+
+fun TextView.setDrawableRight(drawableResId: Int) {
+    setDrawableRight(context.resources.getDrawable(drawableResId)!!)
+}
+
+/**
+ * 动态设置TextView的DrawableRight
+ */
+fun TextView.setDrawableRight(drawable: Drawable) {
+    drawable.setBounds(0, 0, drawable.minimumWidth, drawable.minimumHeight)
+    val originCompoundDrawables = compoundDrawables
+    val drawableLeft = originCompoundDrawables[0].apply {
+        setBounds()
+    }
+    val drawableTop = originCompoundDrawables[1].apply {
+        setBounds()
+    }
+    val drawableBottom = originCompoundDrawables[3].apply {
+        setBounds()
+    }
+    setCompoundDrawablesWithIntrinsicBounds(drawableLeft, drawableTop, drawable, drawableBottom)
+}
+
+fun Drawable?.setBounds() {
+    this?.run {
+        setBounds(0, 0, minimumWidth, minimumHeight)
+    }
+}
+
+/**
+ * 加载图片
+ * @param defaultImgResId 默认图片的资源Id
+ */
+fun ImageView.loadUrlImage(url: String?, defaultImgResId: Int = 0) {
+    ImageLoader.get(context).loader.load(
+        context, LoadOption(
+            LoadOption.Builder()
+                .setUrl(url)
+                .setDefaultImgResId(defaultImgResId)
+        ), this
+    )
+}
+
+/**
+ * 加载资源文件的图片
+ */
+fun ImageView.loadResDrawable(resId: Int, defaultImgResId: Int) {
+    ImageLoader.get(context).loader.load(
+        context, LoadOption(
+            LoadOption.Builder()
+                .setDrawableResId(resId)
+                .setDefaultImgResId(defaultImgResId)
+        ), this
+    )
+}
+
+/**
+ * 加载图片,没有默认图
+ */
+fun ImageView.loadUrlImageNotDefault(url: String?) {
+    loadUrlImage(url, 0)
+}
+
+/**
+ * 加载图片为圆形图片,一般用于头像
+ */
+fun ImageView.loadUrlImageToRound(
+    url: String?,
+    defaultImgResId: Int
+) {
+    ImageLoader.get(context).loader.load(
+        context, LoadOption(
+            LoadOption.Builder()
+                .setUrl(url)
+                .setRound()
+                .setDefaultImgResId(defaultImgResId)
+        ), this
+    )
+}
+
+/**
+ * 加载图片带圆角
+ */
+fun ImageView.loadUrlImageToCorner(
+    url: String?,
+    defaultImgResId: Int
+) {
+    ImageLoader.get(context).loader.load(
+        context, LoadOption(
+            LoadOption.Builder()
+                .setUrl(url)
+                .setDefaultImgResId(defaultImgResId)
+                .setRadius(8f)
+        ), this
+    )
+}
+
+/**
+ * 加载默认图片
+ */
+fun ImageView.loadDefaultImage(resId: Int) {
+    ImageLoader.get(context).loader.load(
+        context, LoadOption(
+            LoadOption.Builder()
+                .setDrawable(context.getDrawable(resId))
+        ), this
+    )
+}

+ 11 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/model/EmptyPlaceholderModel.kt

@@ -0,0 +1,11 @@
+package com.atmob.keyboard_android.model
+
+import java.io.Serializable
+
+/**
+ * 空占位模型
+ */
+data class EmptyPlaceholderModel(
+    // 高度,单位为px
+    val height: Int
+) : Serializable

+ 20 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/model/KeyboardSelectModel.kt

@@ -0,0 +1,20 @@
+package com.atmob.keyboard_android.model
+
+import com.atmob.keyboard_android.R
+import java.io.Serializable
+
+/**
+ * 键盘选择模型
+ */
+data class KeyboardSelectModel(
+    // 键盘图标Url
+    val icon: String,
+    // 键盘名称
+    val name: String,
+    // 是否选中
+    var isSelected: Boolean = false,
+    // 键盘图标,默认资源Id,加载失败时使用
+    val iconDefaultResId: Int = R.mipmap.ic_keyboard_default_icon,
+    // 是否通用键盘
+    val isCommonKeyboard: Boolean = false
+) : Serializable

+ 18 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/imageloader/ImageLoadProgressListener.java

@@ -0,0 +1,18 @@
+package com.atmob.keyboard_android.util.imageloader;
+
+/**
+ * 图片进度回调
+ */
+public interface ImageLoadProgressListener {
+    /**
+     * 进度更新回调
+     *
+     * @param progress 进度百分比
+     */
+    void onProgress(int progress);
+
+    /**
+     * 加载失败
+     */
+    void onLoadFail();
+}

+ 70 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/imageloader/ImageLoader.java

@@ -0,0 +1,70 @@
+package com.atmob.keyboard_android.util.imageloader;
+
+import android.annotation.SuppressLint;
+import android.content.ComponentCallbacks2;
+import android.content.Context;
+import android.content.res.Configuration;
+
+import com.atmob.keyboard_android.util.imageloader.strategy.ILoaderStrategy;
+import com.atmob.keyboard_android.util.imageloader.strategy.impl.GlideLoader;
+
+
+/**
+ * 图片加载器
+ */
+public class ImageLoader {
+    @SuppressLint("StaticFieldLeak")
+    private static volatile ImageLoader instance;
+    private ILoaderStrategy mLoader;
+    private Context mContext;
+
+    public static ImageLoader get(Context context) {
+        if (instance == null) {
+            synchronized (ImageLoader.class) {
+                if (instance == null) {
+                    instance = new ImageLoader()
+                            .holdContext(context.getApplicationContext());
+                }
+            }
+        }
+        return instance;
+    }
+
+    private ImageLoader holdContext(Context context) {
+        mContext = context;
+        return this;
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
+    public void setLoader(ILoaderStrategy loader) {
+        mLoader = loader;
+        mLoader.init(mContext);
+        //注册一个内存低和配置改变监听
+        mContext.getApplicationContext().registerComponentCallbacks(new ComponentCallbacks2() {
+            @Override
+            public void onTrimMemory(int level) {
+                loader.onTrimMemory(level);
+            }
+
+            @Override
+            public void onConfigurationChanged(Configuration newConfig) {
+                loader.onConfigurationChanged(newConfig);
+            }
+
+            @Override
+            public void onLowMemory() {
+                loader.onLowMemory();
+            }
+        });
+    }
+
+    public ILoaderStrategy getLoader() {
+        if (mLoader == null) {
+            setLoader(new GlideLoader());
+        }
+        return mLoader;
+    }
+}

+ 20 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/imageloader/LoadImageCallback.java

@@ -0,0 +1,20 @@
+package com.atmob.keyboard_android.util.imageloader;
+
+import android.graphics.Bitmap;
+
+/**
+ * 加载图片回调
+ */
+public interface LoadImageCallback {
+    /**
+     * 加载成功
+     *
+     * @param bitmap 图片Bitmap对象
+     */
+    void onSuccess(Bitmap bitmap);
+
+    /**
+     * 加载失败
+     */
+    void onFail();
+}

+ 222 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/imageloader/LoadOption.java

@@ -0,0 +1,222 @@
+package com.atmob.keyboard_android.util.imageloader;
+
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+
+import java.io.File;
+
+/**
+ * 图片加载参数
+ */
+public class LoadOption {
+    /**
+     * 要加载的资源类型
+     */
+    private final Class<?> resourceClass;
+    /**
+     * 要加载的资源
+     */
+    private final Object resource;
+    /**
+     * 默认填充的图片ResId
+     */
+    private final int defaultImgResId;
+    /**
+     * 是否圆形,一般用于头像
+     */
+    private final boolean isRound;
+    /**
+     * 是否有圆角
+     */
+    private final boolean isCorner;
+    /**
+     * 左上角圆角半径
+     */
+    private final float topLeftRadius;
+    /**
+     * 右上角圆角半径
+     */
+    private final float topRightRadius;
+    /**
+     * 左下角圆角半径
+     */
+    private final float bottomLeftRadius;
+    /**
+     * 右下角圆角半径
+     */
+    private final float bottomRightRadius;
+
+    public LoadOption(Builder builder) {
+        this.resourceClass = builder.resourceClass;
+        this.resource = builder.resource;
+        this.defaultImgResId = builder.defaultImgResId;
+        this.isRound = builder.isRound;
+        this.isCorner = builder.isCorner;
+        this.topLeftRadius = builder.topLeftRadius;
+        this.topRightRadius = builder.topRightRadius;
+        this.bottomLeftRadius = builder.bottomLeftRadius;
+        this.bottomRightRadius = builder.bottomRightRadius;
+    }
+
+    public Class<?> getResourceClass() {
+        return resourceClass;
+    }
+
+    public Object getResource() {
+        return resource;
+    }
+
+    public int getDefaultImgResId() {
+        return defaultImgResId;
+    }
+
+    public boolean isRound() {
+        return isRound;
+    }
+
+    public boolean isCorner() {
+        return isCorner;
+    }
+
+    public float getTopLeftRadius() {
+        return topLeftRadius;
+    }
+
+    public float getTopRightRadius() {
+        return topRightRadius;
+    }
+
+    public float getBottomLeftRadius() {
+        return bottomLeftRadius;
+    }
+
+    public float getBottomRightRadius() {
+        return bottomRightRadius;
+    }
+
+    public static class Builder {
+        private Class<?> resourceClass;
+        private Object resource;
+        private int defaultImgResId;
+        private boolean isRound;
+        private boolean isCorner;
+        private float topLeftRadius;
+        private float topRightRadius;
+        private float bottomLeftRadius;
+        private float bottomRightRadius;
+
+        /**
+         * 加载Url
+         */
+        public Builder setUrl(String url) {
+            resourceClass = String.class;
+            resource = url;
+            return this;
+        }
+
+        /**
+         * 加载文件File
+         */
+        public Builder setFile(File file) {
+            resourceClass = File.class;
+            resource = file;
+            return this;
+        }
+
+        /**
+         * 加载资源Id数据
+         */
+        public Builder setDrawableResId(int resId) {
+            resourceClass = Integer.class;
+            resource = resId;
+            return this;
+        }
+
+        /**
+         * 加载Drawable
+         */
+        public Builder setDrawable(Drawable drawable) {
+            resourceClass = Drawable.class;
+            resource = drawable;
+            return this;
+        }
+
+        /**
+         * 加载Bitmap
+         */
+        public Builder setBitmap(Bitmap bitmap) {
+            resourceClass = Bitmap.class;
+            resource = bitmap;
+            return this;
+        }
+
+        /**
+         * 设置默认图片资源Id
+         *
+         * @param defaultImgResId 图片资源Id
+         */
+        public Builder setDefaultImgResId(int defaultImgResId) {
+            this.defaultImgResId = defaultImgResId;
+            return this;
+        }
+
+        /**
+         * 是否圆形
+         */
+        public Builder setRound() {
+            this.isRound = true;
+            return this;
+        }
+
+        /**
+         * 设置圆角(4个角都是圆角)
+         */
+        public Builder setRadius(float radius) {
+            setTopLetRadius(radius);
+            setTopRightRadius(radius);
+            setBottomLeftRadius(radius);
+            setBottomRightRadius(radius);
+            return this;
+        }
+
+        /**
+         * 设置左上角圆角半径
+         */
+        public Builder setTopLetRadius(float radius) {
+            isCorner = true;
+            topLeftRadius = radius;
+            return this;
+        }
+
+        /**
+         * 设置右上角的圆角半径
+         */
+        public Builder setTopRightRadius(float radius) {
+            isCorner = true;
+            topRightRadius = radius;
+            return this;
+        }
+
+        /**
+         * 设置左下角的圆角半径
+         */
+        public Builder setBottomLeftRadius(float radius) {
+            isCorner = true;
+            bottomLeftRadius = radius;
+            return this;
+        }
+
+        /**
+         * 设置左下角的圆角半径
+         */
+        public Builder setBottomRightRadius(float radius) {
+            isCorner = true;
+            bottomRightRadius = radius;
+            return this;
+        }
+
+        public LoadOption build() {
+            return new LoadOption(this);
+        }
+    }
+}

+ 38 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/imageloader/progress/ProgressInterceptor.java

@@ -0,0 +1,38 @@
+package com.atmob.keyboard_android.util.imageloader.progress;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import okhttp3.Interceptor;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+/**
+ * 进度拦截器
+ */
+public class ProgressInterceptor implements Interceptor {
+    private static final Map<Object, ProgressListener> listenerMap = new HashMap<>();
+
+    public static void addListener(Object model, ProgressListener listener) {
+        listenerMap.put(model, listener);
+    }
+
+    public static void removeListener(Object model) {
+        listenerMap.remove(model);
+    }
+
+    public static ProgressListener getListener(Object model) {
+        return listenerMap.get(model);
+    }
+
+    @Override
+    public Response intercept(Chain chain) throws IOException {
+        Request request = chain.request();
+        Response response = chain.proceed(request);
+        String url = request.url().toString();
+        ResponseBody body = response.body();
+        return response.newBuilder().body(new ProgressResponseBody(url, body)).build();
+    }
+}

+ 11 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/imageloader/progress/ProgressListener.java

@@ -0,0 +1,11 @@
+package com.atmob.keyboard_android.util.imageloader.progress;
+
+/**
+ * 进度监听
+ */
+public interface ProgressListener {
+    /**
+     * @param progress 进度百分比
+     */
+    void onProgress(int progress);
+}

+ 72 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/imageloader/progress/ProgressResponseBody.java

@@ -0,0 +1,72 @@
+package com.atmob.keyboard_android.util.imageloader.progress;
+
+import java.io.IOException;
+
+import okhttp3.MediaType;
+import okhttp3.ResponseBody;
+import okio.Buffer;
+import okio.BufferedSource;
+import okio.ForwardingSource;
+import okio.Okio;
+import okio.Source;
+
+/**
+ * 支持进度监听的ResponseBody
+ */
+public class ProgressResponseBody extends ResponseBody {
+    private BufferedSource bufferedSource;
+    private final ResponseBody responseBody;
+    private ProgressListener listener;
+
+    public ProgressResponseBody(String url, ResponseBody responseBody) {
+        this.responseBody = responseBody;
+        this.listener = ProgressInterceptor.getListener(url);
+    }
+
+    @Override
+    public MediaType contentType() {
+        return responseBody.contentType();
+    }
+
+    @Override
+    public long contentLength() {
+        return responseBody.contentLength();
+    }
+
+    @Override
+    public BufferedSource source() {
+        if (bufferedSource == null) {
+            bufferedSource = Okio.buffer(new ProgressSource(responseBody.source()));
+        }
+        return bufferedSource;
+    }
+
+    private class ProgressSource extends ForwardingSource {
+        long totalBytesRead = 0;
+        int currentProgress;
+
+        ProgressSource(Source source) {
+            super(source);
+        }
+
+        @Override
+        public long read(Buffer sink, long byteCount) throws IOException {
+            long bytesRead = super.read(sink, byteCount);
+            long fullLength = responseBody.contentLength();
+            if (bytesRead == -1) {
+                totalBytesRead = fullLength;
+            } else {
+                totalBytesRead += bytesRead;
+            }
+            int progress = (int) (100f * totalBytesRead / fullLength);
+            if (listener != null && progress != currentProgress) {
+                listener.onProgress(progress);
+            }
+            if (listener != null && totalBytesRead == fullLength) {
+                listener = null;
+            }
+            currentProgress = progress;
+            return bytesRead;
+        }
+    }
+}

+ 76 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/imageloader/strategy/ILoaderStrategy.java

@@ -0,0 +1,76 @@
+package com.atmob.keyboard_android.util.imageloader.strategy;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.widget.ImageView;
+
+import com.atmob.keyboard_android.util.imageloader.ImageLoadProgressListener;
+import com.atmob.keyboard_android.util.imageloader.LoadImageCallback;
+import com.atmob.keyboard_android.util.imageloader.LoadOption;
+
+
+/**
+ * 图片加载器策略接口
+ */
+public interface ILoaderStrategy {
+    /**
+     * 初始化,在application的onCreate中初始化,该方法存在意义是为了有些框架需要在Application中初始化
+     */
+    void init(Context context);
+
+    /**
+     * 加载图片
+     *
+     * @param option     加载可选项
+     * @param targetView 目标ImageView
+     */
+    void load(Context context, LoadOption option, ImageView targetView);
+
+    /**
+     * 加载图片,并设置进度监听
+     */
+    void load(Context context, String url, ImageView targetView, ImageLoadProgressListener progressListener);
+
+    /**
+     * 加载图片为Bitmap并回调
+     *
+     * @param option   加载可选项
+     * @param callback 回调
+     */
+    void loadToBitmap(Context context, LoadOption option, LoadImageCallback callback);
+
+    /**
+     * 清除内存缓存
+     */
+    void clearMemoryCache(int level);
+
+    /**
+     * 清除磁盘缓存
+     */
+    void clearDiskCache();
+
+    /**
+     * 暂停请求,一般在ListView或RecyclerView滚动时调用,停止加载快速滚动的图片数据
+     */
+    void pause(Context context);
+
+    /**
+     * 恢复请求,在ListView或RecyclerView滚动停止时调用
+     */
+    void resume(Context context);
+
+    /**
+     * 转调onLowMemory()给Loader处理
+     */
+    void onLowMemory();
+
+    /**
+     * 转调onTrimMemory()给Loader处理
+     */
+    void onTrimMemory(int level);
+
+    /**
+     * 转调onConfigurationChanged()给Loader处理
+     */
+    void onConfigurationChanged(Configuration newConfig);
+}

+ 264 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/imageloader/strategy/impl/GlideLoader.java

@@ -0,0 +1,264 @@
+package com.atmob.keyboard_android.util.imageloader.strategy.impl;
+
+import android.content.ComponentCallbacks2;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.atmob.keyboard_android.util.imageloader.ImageLoadProgressListener;
+import com.atmob.keyboard_android.util.imageloader.LoadImageCallback;
+import com.atmob.keyboard_android.util.imageloader.LoadOption;
+import com.atmob.keyboard_android.util.imageloader.progress.ProgressInterceptor;
+import com.atmob.keyboard_android.util.imageloader.progress.ProgressListener;
+import com.atmob.keyboard_android.util.imageloader.strategy.ILoaderStrategy;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.RequestBuilder;
+import com.bumptech.glide.RequestManager;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.MultiTransformation;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.bumptech.glide.load.engine.GlideException;
+import com.bumptech.glide.request.RequestListener;
+import com.bumptech.glide.request.RequestOptions;
+import com.bumptech.glide.request.target.BitmapImageViewTarget;
+import com.bumptech.glide.request.target.SimpleTarget;
+import com.bumptech.glide.request.target.Target;
+import com.bumptech.glide.request.transition.Transition;
+
+import java.io.File;
+
+import jp.wasabeef.glide.transformations.RoundedCornersTransformation;
+
+/**
+ * Glide实现
+ */
+public class GlideLoader implements ILoaderStrategy {
+    private Context mApplicationContext;
+    private final RequestOptions mRequestOptions;
+    private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+
+    public GlideLoader() {
+        mRequestOptions = new RequestOptions()
+                .dontAnimate()
+                //内存缓存处理图,磁盘缓存原图
+                .skipMemoryCache(false)
+                .diskCacheStrategy(DiskCacheStrategy.RESOURCE);
+    }
+
+    @Override
+    public void init(Context context) {
+        mApplicationContext = context.getApplicationContext();
+    }
+
+    @Override
+    public void load(Context context, LoadOption option, ImageView targetView) {
+        //复制一份Options
+        RequestOptions requestOptions = mRequestOptions.clone();
+        //根据option中的资源类型决定加载的资源类型,再加载加载
+        RequestBuilder<?> requestBuilder = adapterMultipleResources(context, option);
+        if (requestBuilder == null) {
+            //不是能处理的类型,一般不会走到这里
+            return;
+        }
+        //处理圆形
+        if (option.isRound()) {
+            requestBuilder = requestBuilder.circleCrop();
+        }
+        //处理圆角
+        requestBuilder = requestBuilder
+                .apply(RequestOptions.bitmapTransform(new MultiTransformation<>(
+                        //左上角圆角
+                        new RoundedCornersTransformation((int) option.getTopLeftRadius(), 0, RoundedCornersTransformation.CornerType.TOP_LEFT),
+                        //右上角圆角
+                        new RoundedCornersTransformation((int) option.getTopRightRadius(), 0, RoundedCornersTransformation.CornerType.TOP_RIGHT),
+                        //左下角圆角
+                        new RoundedCornersTransformation((int) option.getBottomLeftRadius(), 0, RoundedCornersTransformation.CornerType.BOTTOM_LEFT),
+                        //右下角圆角
+                        new RoundedCornersTransformation((int) option.getBottomRightRadius(), 0, RoundedCornersTransformation.CornerType.BOTTOM_RIGHT))));
+        //处理默认占位图
+        int defaultImage = option.getDefaultImgResId();
+        if (defaultImage != 0) {
+            requestBuilder = requestBuilder
+                    .placeholder(defaultImage)
+                    .error(defaultImage);
+        }
+        requestBuilder
+                //复用标准配置
+                .apply(requestOptions)
+                //加载
+                .into(targetView);
+    }
+
+    @Override
+    public void load(Context context, String url, ImageView targetView, ImageLoadProgressListener progressListener) {
+        Glide.with(targetView)
+                .asBitmap()
+                .skipMemoryCache(true)
+                .diskCacheStrategy(DiskCacheStrategy.NONE)
+                .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
+                .addListener(new RequestListener<Bitmap>() {
+                    @Override
+                    public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Bitmap> target, boolean isFirstResource) {
+                        ProgressInterceptor.removeListener(model);
+                        return false;
+                    }
+
+                    @Override
+                    public boolean onResourceReady(Bitmap resource, Object model, Target<Bitmap> target, DataSource dataSource, boolean isFirstResource) {
+                        ProgressInterceptor.removeListener(model);
+                        return false;
+                    }
+                })
+                .load(url)
+                .into(new BitmapImageViewTarget(targetView) {
+                    @Override
+                    public void onLoadStarted(@Nullable Drawable placeholder) {
+                        super.onLoadStarted(placeholder);
+                        if (progressListener != null) {
+                            progressListener.onProgress(0);
+                        }
+                        ProgressInterceptor.addListener(url, new ProgressListener() {
+                            @Override
+                            public void onProgress(final int progress) {
+                                mMainHandler.post(new Runnable() {
+                                    @Override
+                                    public void run() {
+                                        if (progressListener != null) {
+                                            progressListener.onProgress(progress);
+                                        }
+                                    }
+                                });
+                            }
+                        });
+                    }
+
+                    @Override
+                    public void onLoadFailed(@Nullable Drawable errorDrawable) {
+                        super.onLoadFailed(errorDrawable);
+                        if (progressListener != null) {
+                            progressListener.onLoadFail();
+                        }
+                    }
+                });
+    }
+
+    @Override
+    public void loadToBitmap(Context context, LoadOption option, LoadImageCallback callback) {
+        RequestBuilder<Bitmap> requestBuilder = Glide.with(context).asBitmap();
+        adapterMultipleResources(requestBuilder, option).into(new SimpleTarget<Bitmap>() {
+            @Override
+            public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
+                if (callback != null) {
+                    callback.onSuccess(resource);
+                }
+            }
+
+            @Override
+            public void onLoadFailed(@Nullable Drawable errorDrawable) {
+                super.onLoadFailed(errorDrawable);
+                if (callback != null) {
+                    callback.onFail();
+                }
+            }
+        });
+    }
+
+    private RequestBuilder<?> adapterMultipleResources(Context context, LoadOption option) {
+        RequestManager manager = Glide.with(context);
+        return adapterMultipleResources(manager, option);
+    }
+
+    private RequestBuilder<?> adapterMultipleResources(RequestManager requestManager, LoadOption option) {
+        Class<?> resourceClass = option.getResourceClass();
+        Object resource = option.getResource();
+        RequestBuilder<?> requestBuilder = null;
+        if (String.class.equals(resourceClass)) {
+            //处理转义字符
+            String url = replaceEscape((String) resource);
+            requestBuilder = requestManager.load(url);
+        } else if (File.class.equals(resourceClass)) {
+            requestBuilder = requestManager.load((File) resource);
+        } else if (Integer.class.equals(resourceClass)) {
+            requestBuilder = requestManager.load((Integer) resource);
+        } else if (Drawable.class.equals(resourceClass)) {
+            requestBuilder = requestManager.load((Drawable) resource);
+        } else if (Bitmap.class.equals(resourceClass)) {
+            requestBuilder = requestManager.load((Bitmap) resource);
+        }
+        return requestBuilder;
+    }
+
+    private <T> RequestBuilder<T> adapterMultipleResources(RequestBuilder<T> requestBuilder, LoadOption option) {
+        Class<?> resourceClass = option.getResourceClass();
+        Object resource = option.getResource();
+        if (String.class.equals(resourceClass)) {
+            //处理转义字符
+            String url = replaceEscape((String) resource);
+            requestBuilder = requestBuilder.load(url);
+        } else if (File.class.equals(resourceClass)) {
+            requestBuilder = requestBuilder.load((File) resource);
+        } else if (Drawable.class.equals(resourceClass)) {
+            requestBuilder = requestBuilder.load((Drawable) resource);
+        } else if (Bitmap.class.equals(resourceClass)) {
+            requestBuilder = requestBuilder.load((Bitmap) resource);
+        }
+        return requestBuilder;
+    }
+
+    @Override
+    public void clearMemoryCache(int level) {
+        if (level == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
+            Glide.get(mApplicationContext).clearMemory();
+        }
+        Glide.get(mApplicationContext).trimMemory(level);
+    }
+
+    @Override
+    public void clearDiskCache() {
+        Thread thread = new Thread(() -> Glide.get(mApplicationContext).clearDiskCache());
+        thread.start();
+    }
+
+    @Override
+    public void pause(Context context) {
+        Glide.with(context).pauseRequests();
+    }
+
+    @Override
+    public void resume(Context context) {
+        Glide.with(context).resumeRequests();
+    }
+
+    @Override
+    public void onLowMemory() {
+        Glide.get(mApplicationContext).onLowMemory();
+    }
+
+    @Override
+    public void onTrimMemory(int level) {
+        Glide.get(mApplicationContext).onTrimMemory(level);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        Glide.get(mApplicationContext).onConfigurationChanged(newConfig);
+    }
+
+    /**
+     * 替换url中的换行符
+     */
+    private String replaceEscape(String url) {
+        if (TextUtils.isEmpty(url)) {
+            return url;
+        }
+        return url.replaceAll("\\\\", "");
+    }
+}

+ 48 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/listener/DelayOnClickListener.java

@@ -0,0 +1,48 @@
+package com.atmob.keyboard_android.util.listener;
+
+import android.view.View;
+
+/**
+ * 用于点击事件防重点、防暴击,设置点击事件,设置该类,实现抽象方法
+ */
+public class DelayOnClickListener implements View.OnClickListener {
+    /**
+     * 默认延时时间
+     */
+    private static final int DEFAULT_DELAY_TIME = 300;
+    /**
+     * 上一次的点击时间
+     */
+    private long mLastClickTime;
+    /**
+     * 延迟时间
+     */
+    private int mDelayTime;
+    /**
+     * 包裹的监听器
+     */
+    private View.OnClickListener mWrapperListener;
+
+    public DelayOnClickListener(View.OnClickListener wrapperListener) {
+        this(DEFAULT_DELAY_TIME, wrapperListener);
+    }
+
+    public DelayOnClickListener(int delayTime, View.OnClickListener wrapperListener) {
+        if (delayTime < 0) {
+            return;
+        }
+        mDelayTime = delayTime;
+        mWrapperListener = wrapperListener;
+    }
+
+    @Override
+    public final void onClick(View view) {
+        if (System.currentTimeMillis() - mLastClickTime < mDelayTime) {
+            return;
+        }
+        if (mWrapperListener != null) {
+            mWrapperListener.onClick(view);
+        }
+        mLastClickTime = System.currentTimeMillis();
+    }
+}

+ 47 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/util/recyclerview/GridDivider.kt

@@ -0,0 +1,47 @@
+package com.atmob.keyboard_android.util.recyclerview
+
+import android.graphics.Rect
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * 网格布局分割线
+ */
+class GridDivider(
+    // 列数
+    private val spanCount: Int,
+    // 分割线宽度,单位为px
+    private val spacing: Int
+) : RecyclerView.ItemDecoration() {
+
+    override fun getItemOffsets(
+        outRect: Rect,
+        view: View,
+        parent: RecyclerView,
+        state: RecyclerView.State
+    ) {
+        val position = parent.getChildAdapterPosition(view)
+        // 当前列索引
+        val column = position % spanCount
+
+        // 右边距,非最后一列添加间距
+        outRect.right = if (column < spanCount - 1) {
+            spacing
+        } else {
+            0
+        }
+
+        // 下边距,非最后一行添加间距
+//        val totalItemCount = parent.adapter?.itemCount ?: 0
+//        val row = position / spanCount
+//        val totalRows = (totalItemCount + spanCount - 1) / spanCount
+//        outRect.bottom = if (row < totalRows - 1) {
+//            spacing
+//        } else {
+//            0
+//        }
+
+        // 都添加下边距
+        outRect.bottom = spacing
+    }
+}

+ 44 - 0
plugins/keyboard_android/android/src/main/kotlin/com/bumptech/glide/integration/okhttp3/OkHttpGlideModule.java

@@ -0,0 +1,44 @@
+package com.bumptech.glide.integration.okhttp3;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import com.atmob.keyboard_android.util.imageloader.progress.ProgressInterceptor;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.GlideBuilder;
+import com.bumptech.glide.Registry;
+import com.bumptech.glide.load.model.GlideUrl;
+
+import java.io.InputStream;
+
+import okhttp3.OkHttpClient;
+
+/**
+ * A {@link com.bumptech.glide.module.GlideModule} implementation to replace Glide's default
+ * {@link java.net.HttpURLConnection} based {@link com.bumptech.glide.load.model.ModelLoader}
+ * with an OkHttp based {@link com.bumptech.glide.load.model.ModelLoader}.
+ *
+ * <p> If you're using gradle, you can include this module simply by depending on the aar, the
+ * module will be merged in by manifest merger. For other build systems or for more more
+ * information, see {@link com.bumptech.glide.module.GlideModule}. </p>
+ *
+ * @deprecated Replaced by {@link OkHttpLibraryGlideModule} for Applications that use Glide's
+ * annotations.
+ */
+@Deprecated
+public class OkHttpGlideModule implements com.bumptech.glide.module.GlideModule {
+  @Override
+  public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
+    // Do nothing.
+  }
+
+  @Override
+  public void registerComponents(@NonNull Context context, @NonNull Glide glide, Registry registry) {
+    OkHttpClient okHttpClient = new OkHttpClient.Builder()
+            //添加进度拦截器
+            .addInterceptor(new ProgressInterceptor())
+            .build();
+    registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(okHttpClient));
+  }
+}

+ 30 - 0
plugins/keyboard_android/android/src/main/kotlin/com/bumptech/glide/integration/okhttp3/OkHttpLibraryGlideModule.java

@@ -0,0 +1,30 @@
+package com.bumptech.glide.integration.okhttp3;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.Registry;
+import com.bumptech.glide.annotation.GlideModule;
+import com.bumptech.glide.load.model.GlideUrl;
+import com.bumptech.glide.module.AppGlideModule;
+import com.bumptech.glide.module.LibraryGlideModule;
+
+import java.io.InputStream;
+
+/**
+ * Registers OkHttp related classes via Glide's annotation processor.
+ *
+ * <p>For Applications that depend on this library and include an
+ * {@link AppGlideModule} and Glide's annotation processor, this class
+ * will be automatically included.
+ */
+@GlideModule
+public final class OkHttpLibraryGlideModule extends LibraryGlideModule {
+  @Override
+  public void registerComponents(@NonNull Context context, @NonNull Glide glide,
+                                 @NonNull Registry registry) {
+    registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
+  }
+}

+ 115 - 0
plugins/keyboard_android/android/src/main/kotlin/com/bumptech/glide/integration/okhttp3/OkHttpStreamFetcher.java

@@ -0,0 +1,115 @@
+package com.bumptech.glide.integration.okhttp3;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.bumptech.glide.Priority;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.HttpException;
+import com.bumptech.glide.load.data.DataFetcher;
+import com.bumptech.glide.load.model.GlideUrl;
+import com.bumptech.glide.util.ContentLengthInputStream;
+import com.bumptech.glide.util.Preconditions;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+
+import okhttp3.Call;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+/**
+ * Fetches an {@link InputStream} using the okhttp library.
+ */
+public class OkHttpStreamFetcher implements DataFetcher<InputStream>, okhttp3.Callback {
+  private static final String TAG = "OkHttpFetcher";
+  private final Call.Factory client;
+  private final GlideUrl url;
+  private InputStream stream;
+  private ResponseBody responseBody;
+  private DataCallback<? super InputStream> callback;
+  // call may be accessed on the main thread while the object is in use on other threads. All other
+  // accesses to variables may occur on different threads, but only one at a time.
+  private volatile Call call;
+
+  // Public API.
+  @SuppressWarnings("WeakerAccess")
+  public OkHttpStreamFetcher(Call.Factory client, GlideUrl url) {
+    this.client = client;
+    this.url = url;
+  }
+
+  @Override
+  public void loadData(@NonNull Priority priority,
+      @NonNull final DataCallback<? super InputStream> callback) {
+    Request.Builder requestBuilder = new Request.Builder().url(url.toStringUrl());
+    for (Map.Entry<String, String> headerEntry : url.getHeaders().entrySet()) {
+      String key = headerEntry.getKey();
+      requestBuilder.addHeader(key, headerEntry.getValue());
+    }
+    Request request = requestBuilder.build();
+    this.callback = callback;
+
+    call = client.newCall(request);
+    call.enqueue(this);
+  }
+
+  @Override
+  public void onFailure(@NonNull Call call, @NonNull IOException e) {
+    if (Log.isLoggable(TAG, Log.DEBUG)) {
+      Log.d(TAG, "OkHttp failed to obtain result", e);
+    }
+
+    callback.onLoadFailed(e);
+  }
+
+  @Override
+  public void onResponse(@NonNull Call call, @NonNull Response response) {
+    responseBody = response.body();
+    if (response.isSuccessful()) {
+      long contentLength = Preconditions.checkNotNull(responseBody).contentLength();
+      stream = ContentLengthInputStream.obtain(responseBody.byteStream(), contentLength);
+      callback.onDataReady(stream);
+    } else {
+      callback.onLoadFailed(new HttpException(response.message(), response.code()));
+    }
+  }
+
+  @Override
+  public void cleanup() {
+    try {
+      if (stream != null) {
+        stream.close();
+      }
+    } catch (IOException e) {
+      // Ignored
+    }
+    if (responseBody != null) {
+      responseBody.close();
+    }
+    callback = null;
+  }
+
+  @Override
+  public void cancel() {
+    Call local = call;
+    if (local != null) {
+      local.cancel();
+    }
+  }
+
+  @NonNull
+  @Override
+  public Class<InputStream> getDataClass() {
+    return InputStream.class;
+  }
+
+  @NonNull
+  @Override
+  public DataSource getDataSource() {
+    return DataSource.REMOTE;
+  }
+}

+ 88 - 0
plugins/keyboard_android/android/src/main/kotlin/com/bumptech/glide/integration/okhttp3/OkHttpUrlLoader.java

@@ -0,0 +1,88 @@
+package com.bumptech.glide.integration.okhttp3;
+
+
+import androidx.annotation.NonNull;
+
+import com.bumptech.glide.load.Options;
+import com.bumptech.glide.load.model.GlideUrl;
+import com.bumptech.glide.load.model.ModelLoader;
+import com.bumptech.glide.load.model.ModelLoaderFactory;
+import com.bumptech.glide.load.model.MultiModelLoaderFactory;
+
+import java.io.InputStream;
+
+import okhttp3.Call;
+import okhttp3.OkHttpClient;
+
+/**
+ * A simple model loader for fetching media over http/https using OkHttp.
+ */
+public class OkHttpUrlLoader implements ModelLoader<GlideUrl, InputStream> {
+
+  private final Call.Factory client;
+
+  // Public API.
+  @SuppressWarnings("WeakerAccess")
+  public OkHttpUrlLoader(@NonNull Call.Factory client) {
+    this.client = client;
+  }
+
+  @Override
+  public boolean handles(@NonNull GlideUrl url) {
+    return true;
+  }
+
+  @Override
+  public LoadData<InputStream> buildLoadData(@NonNull GlideUrl model, int width, int height,
+      @NonNull Options options) {
+    return new LoadData<>(model, new OkHttpStreamFetcher(client, model));
+  }
+
+  /**
+   * The default factory for {@link OkHttpUrlLoader}s.
+   */
+  // Public API.
+  @SuppressWarnings("WeakerAccess")
+  public static class Factory implements ModelLoaderFactory<GlideUrl, InputStream> {
+    private static volatile Call.Factory internalClient;
+    private final Call.Factory client;
+
+    private static Call.Factory getInternalClient() {
+      if (internalClient == null) {
+        synchronized (Factory.class) {
+          if (internalClient == null) {
+            internalClient = new OkHttpClient();
+          }
+        }
+      }
+      return internalClient;
+    }
+
+    /**
+     * Constructor for a new Factory that runs requests using a static singleton client.
+     */
+    public Factory() {
+      this(getInternalClient());
+    }
+
+    /**
+     * Constructor for a new Factory that runs requests using given client.
+     *
+     * @param client this is typically an instance of {@code OkHttpClient}.
+     */
+    public Factory(@NonNull Call.Factory client) {
+      this.client = client;
+    }
+
+    @NonNull
+    @Override
+    public ModelLoader<GlideUrl, InputStream> build(MultiModelLoaderFactory multiFactory) {
+      return new OkHttpUrlLoader(client);
+    }
+
+    @Override
+    public void teardown() {
+      // Do nothing, this instance doesn't own the client.
+    }
+  }
+}

+ 7 - 0
plugins/keyboard_android/android/src/main/res/drawable/bg_keyboard_normal.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+
+    <corners android:radius="14dp" />
+    <solid android:color="@color/text_color_white" />
+</shape>

+ 11 - 0
plugins/keyboard_android/android/src/main/res/drawable/bg_keyboard_selected.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+
+    <gradient
+        android:angle="180"
+        android:endColor="@color/text_color_brand"
+        android:startColor="@color/text_color_auxiliary1" />
+
+    <corners android:radius="14dp" />
+</shape>

+ 9 - 1
plugins/keyboard_android/android/src/main/res/layout/component_key_board_container.xml

@@ -1,7 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content">
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
 
     <!-- 工具栏 -->
     <com.atmob.keyboard_android.component.ToolBarComponent
@@ -13,4 +14,11 @@
     <!-- 拼音键盘 -->
 
     <!-- AI键盘 -->
+
+    <!-- 键盘选择页 -->
+    <com.atmob.keyboard_android.component.KeyboardSelectComponent
+        android:id="@+id/keyboard_select_component"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1" />
 </LinearLayout>

+ 27 - 0
plugins/keyboard_android/android/src/main/res/layout/component_keyboard_select.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.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="@dimen/keyboard_content_height"
+    tools:background="@mipmap/bg_keyboard">
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/list"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:paddingStart="12dp"
+        android:paddingEnd="12dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <ImageView
+        android:id="@+id/back_btn"
+        android:layout_width="32dp"
+        android:layout_height="32dp"
+        android:layout_marginStart="12dp"
+        android:layout_marginTop="14dp"
+        android:src="@mipmap/ic_back_btn"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 5 - 0
plugins/keyboard_android/android/src/main/res/layout/item_empty_placeholder.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/item_placeholder"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content" />

+ 31 - 0
plugins/keyboard_android/android/src/main/res/layout/item_keyboard_select.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <LinearLayout
+        android:id="@+id/item_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="@drawable/bg_keyboard_selected"
+        android:gravity="center_horizontal"
+        android:orientation="vertical">
+
+        <ImageView
+            android:id="@+id/icon"
+            android:layout_width="36dp"
+            android:layout_height="36dp"
+            android:layout_marginTop="10dp"
+            android:src="@mipmap/ic_common_keyboard_icon" />
+
+        <TextView
+            android:id="@+id/name"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="8dp"
+            android:layout_marginBottom="10dp"
+            android:text="@string/common_keyboard"
+            android:textColor="@color/text_color_white"
+            android:textStyle="bold" />
+    </LinearLayout>
+</FrameLayout>

BIN
plugins/keyboard_android/android/src/main/res/mipmap-xxxhdpi/ic_common_keyboard_icon.png


BIN
plugins/keyboard_android/android/src/main/res/mipmap-xxxhdpi/ic_keyboard_default_icon.png


+ 2 - 0
plugins/keyboard_android/android/src/main/res/values/dimens.xml

@@ -2,4 +2,6 @@
 <resources>
     <!-- 键盘高度 -->
     <dimen name="keyboard_height">292dp</dimen>
+    <!-- 内容高度,不包含工具栏 -->
+    <dimen name="keyboard_content_height">242dp</dimen>
 </resources>

+ 1 - 0
plugins/keyboard_android/android/src/main/res/values/string.xml

@@ -10,4 +10,5 @@
     <string name="mode_help_chat">帮聊</string>
     <string name="mode_teach_you_say">教你说</string>
     <string name="mode_open_remarks">开场白</string>
+    <string name="common_keyboard">通用键盘</string>
 </resources>