├── selecttext ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ ├── drawable-xhdpi │ │ │ └── ic_arrow.png │ │ ├── drawable-xxhdpi │ │ │ └── ic_arrow.png │ │ ├── drawable │ │ │ └── shape_color_4c4c4c_radius_8.xml │ │ └── layout │ │ │ ├── item_select_text_pop.xml │ │ │ └── pop_operate.xml │ │ └── java │ │ └── com │ │ └── xiaoguang │ │ └── selecttext │ │ ├── SelectImageSpan.kt │ │ ├── SelectTextPopAdapter.kt │ │ ├── SelectUtils.kt │ │ └── SelectTextHelper.kt └── build.gradle ├── app ├── .gitignore ├── select ├── src │ └── main │ │ ├── res │ │ ├── drawable-xhdpi │ │ │ ├── emoji_00.png │ │ │ ├── emoji_01.png │ │ │ ├── emoji_02.png │ │ │ ├── emoji_03.png │ │ │ ├── emoji_04.png │ │ │ ├── emoji_gif.gif │ │ │ ├── emoji_gif2.gif │ │ │ └── ic_arrow_666.png │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── drawable-xxhdpi │ │ │ ├── ic_msg_copy.png │ │ │ ├── ic_msg_quote.png │ │ │ ├── ic_msg_collect.png │ │ │ ├── ic_msg_delete.png │ │ │ ├── ic_msg_forward.png │ │ │ ├── ic_msg_resend.png │ │ │ ├── ic_msg_rollback.png │ │ │ ├── ic_msg_select.png │ │ │ ├── ic_msg_select_all.png │ │ │ └── ic_msg_cancel_upload.png │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ ├── drawable │ │ │ ├── shape_color_4c4c4c_radius_8.xml │ │ │ ├── shape_color_666666_radius_8.xml │ │ │ └── shape_color_f2f3f5_radius_8.xml │ │ └── layout │ │ │ ├── dialog_select_text.xml │ │ │ ├── item_msg_img.xml │ │ │ ├── item_msg_text.xml │ │ │ ├── item_msg_link.xml │ │ │ └── activity_main.xml │ │ ├── java │ │ └── com │ │ │ └── xiaoguang │ │ │ └── selecttextview │ │ │ ├── MsgBean.kt │ │ │ ├── SelectTextEvent.kt │ │ │ ├── SelectTextEventBus.kt │ │ │ ├── SelectTextDialog.kt │ │ │ ├── CustomPop.kt │ │ │ ├── MainActivity.kt │ │ │ └── MsgAdapter.kt │ │ └── AndroidManifest.xml └── build.gradle ├── settings.gradle ├── Untitled ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── gradlew.bat ├── gradlew └── README.md /selecttext/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':selecttext' -------------------------------------------------------------------------------- /Untitled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/Untitled -------------------------------------------------------------------------------- /app/select: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/select -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 2 | android.useAndroidX=true 3 | android.enableJetifier=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /selecttext/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/emoji_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/drawable-xhdpi/emoji_00.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/emoji_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/drawable-xhdpi/emoji_01.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/emoji_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/drawable-xhdpi/emoji_02.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/emoji_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/drawable-xhdpi/emoji_03.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/emoji_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/drawable-xhdpi/emoji_04.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/emoji_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/drawable-xhdpi/emoji_gif.gif -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/emoji_gif2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/drawable-xhdpi/emoji_gif2.gif -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_arrow_666.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/drawable-xhdpi/ic_arrow_666.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_msg_copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/drawable-xxhdpi/ic_msg_copy.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_msg_quote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/drawable-xxhdpi/ic_msg_quote.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_msg_collect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/drawable-xxhdpi/ic_msg_collect.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_msg_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/drawable-xxhdpi/ic_msg_delete.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_msg_forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/drawable-xxhdpi/ic_msg_forward.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_msg_resend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/drawable-xxhdpi/ic_msg_resend.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_msg_rollback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/drawable-xxhdpi/ic_msg_rollback.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_msg_select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/drawable-xxhdpi/ic_msg_select.png -------------------------------------------------------------------------------- /selecttext/src/main/res/drawable-xhdpi/ic_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/selecttext/src/main/res/drawable-xhdpi/ic_arrow.png -------------------------------------------------------------------------------- /selecttext/src/main/res/drawable-xxhdpi/ic_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/selecttext/src/main/res/drawable-xxhdpi/ic_arrow.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_msg_select_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/drawable-xxhdpi/ic_msg_select_all.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_msg_cancel_upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITxiaoguang/SelectTextHelper/HEAD/app/src/main/res/drawable-xxhdpi/ic_msg_cancel_upload.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #39A6FF 4 | #4D39A6FF 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/xiaoguang/selecttextview/MsgBean.kt: -------------------------------------------------------------------------------- 1 | package com.xiaoguang.selecttextview 2 | 3 | /** 4 | * hxg 2021/9/13 16:28 qq:929842234 5 | */ 6 | class MsgBean(// 消息类型 7 | val type: Int, // 判断消息方向,是否是接收到的消息 8 | val isReceive: Boolean, // 内容 9 | val content: String?) -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Sep 13 11:46:38 CST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_color_4c4c4c_radius_8.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_color_666666_radius_8.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_color_f2f3f5_radius_8.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /selecttext/src/main/res/drawable/shape_color_4c4c4c_radius_8.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/xiaoguang/selecttextview/SelectTextEvent.kt: -------------------------------------------------------------------------------- 1 | package com.xiaoguang.selecttextview 2 | 3 | import java.io.Serializable 4 | 5 | /** 6 | * 选择文本的event 7 | * hxg 2021/08/31 15:26 qq:929842234 8 | */ 9 | class SelectTextEvent( 10 | // 关闭所有弹窗 dismissAllPop 11 | // 延迟关闭所有弹窗 dismissAllPopDelayed 12 | // 关闭操作弹窗 dismissOperatePop 13 | val type: String) : Serializable -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | SelectTextDemo 3 | 4 | 复制 5 | 全选 6 | 转发 7 | 收藏 8 | 多选 9 | 引用 10 | 撤回 11 | 删除 12 | 13 | -------------------------------------------------------------------------------- /selecttext/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | // 1. 【必加】 启用 android-maven 插件 4 | id 'com.github.dcendents.android-maven' 5 | id 'kotlin-android' 6 | } 7 | // 2. 【必加】 关联Github地址, 格式为 com.github.(用户名) 8 | group = 'com.github.ITxiaoguang' 9 | 10 | android { 11 | compileSdk 31 12 | 13 | defaultConfig { 14 | minSdk 14 15 | } 16 | 17 | compileOptions { 18 | sourceCompatibility JavaVersion.VERSION_1_8 19 | targetCompatibility JavaVersion.VERSION_1_8 20 | } 21 | 22 | kotlinOptions { 23 | jvmTarget = '1.8' 24 | } 25 | 26 | } 27 | 28 | dependencies { 29 | implementation 'com.google.android.material:material:1.6.1' 30 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 31 8 | 9 | defaultConfig { 10 | applicationId "com.xiaoguang.selecttextview" 11 | minSdk 14 12 | targetSdk 30 13 | versionCode 2 14 | versionName "1.1.0" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | compileOptions { 24 | sourceCompatibility JavaVersion.VERSION_1_8 25 | targetCompatibility JavaVersion.VERSION_1_8 26 | } 27 | kotlinOptions { 28 | jvmTarget = '1.8' 29 | } 30 | } 31 | 32 | dependencies { 33 | implementation 'com.google.android.material:material:1.6.1' 34 | 35 | // Eventbus 36 | implementation 'org.greenrobot:eventbus:3.1.1' 37 | // Glide 加载富文本图片 38 | implementation 'com.github.bumptech.glide:glide:4.12.0' 39 | // 富文本 https://github.com/zzhoujay/RichText 40 | implementation 'com.zzhoujay.richtext:richtext:3.0.8' 41 | 42 | implementation project(path: ':selecttext') 43 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_select_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 18 | 19 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 17 | 18 | 19 | 25 | -------------------------------------------------------------------------------- /selecttext/src/main/res/layout/item_select_text_pop.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 19 | 20 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_msg_img.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 23 | 24 | 31 | 32 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /selecttext/src/main/java/com/xiaoguang/selecttext/SelectImageSpan.kt: -------------------------------------------------------------------------------- 1 | package com.xiaoguang.selecttext 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Paint 5 | import android.graphics.drawable.Drawable 6 | import android.text.style.ImageSpan 7 | import androidx.annotation.ColorInt 8 | 9 | /** 10 | * 继承ImageSpan,绘制图片背景 11 | * https://developer.android.google.cn/reference/android/text/style/DynamicDrawableSpan 12 | * 13 | * Create by gnmmdk 14 | */ 15 | class SelectImageSpan(drawable: Drawable, @ColorInt var bgColor: Int, verticalAlignment: Int) : 16 | ImageSpan(drawable, verticalAlignment) { 17 | 18 | /** 19 | * 重写 draw 方法 20 | * 绘制背景 21 | */ 22 | override fun draw( 23 | canvas: Canvas, 24 | text: CharSequence?, 25 | start: Int, 26 | end: Int, 27 | x: Float, 28 | top: Int, 29 | y: Int, 30 | bottom: Int, 31 | paint: Paint 32 | ) { 33 | // From super.draw(canvas, text, start, end, x, top, y, bottom, paint) 34 | val d = drawable 35 | canvas.save() 36 | 37 | // From gnmmdk 修改canvas paint颜色实现 38 | paint.color = bgColor 39 | canvas.drawRect(x, top.toFloat(), x + d.bounds.right, bottom.toFloat(), paint) 40 | 41 | // From super.draw(canvas, text, start, end, x, top, y, bottom, paint) 42 | var transY = bottom - d.bounds.bottom 43 | if (mVerticalAlignment == ALIGN_BASELINE) { 44 | transY -= paint.fontMetricsInt.descent 45 | } else if (mVerticalAlignment == ALIGN_CENTER) { 46 | transY = top + (bottom - top) / 2 - d.bounds.height() / 2 47 | } 48 | canvas.translate(x, transY.toFloat()) 49 | d.draw(canvas) 50 | canvas.restore() 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /selecttext/src/main/res/layout/pop_operate.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 21 | 22 | 36 | 37 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_msg_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 18 | 19 | 25 | 26 | 34 | 35 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_msg_link.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 18 | 19 | 25 | 26 | 34 | 35 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/xiaoguang/selecttextview/SelectTextEventBus.kt: -------------------------------------------------------------------------------- 1 | package com.xiaoguang.selecttextview 2 | 3 | import org.greenrobot.eventbus.EventBus 4 | 5 | /** 6 | * 选择文本的事件总线 7 | * 这里的方法比较low,用户自行实现 8 | * hxg 2021/9/2 14:26 qq:929842234 9 | */ 10 | class SelectTextEventBus { 11 | private val typesBySubscriber: MutableMap>> 12 | 13 | init { 14 | typesBySubscriber = HashMap() 15 | } 16 | 17 | companion object { 18 | @Volatile 19 | private var defaultInstance: SelectTextEventBus? = null 20 | 21 | val instance: SelectTextEventBus 22 | get() { 23 | if (defaultInstance == null) { 24 | synchronized(SelectTextEventBus::class.java) { 25 | if (defaultInstance == null) { 26 | defaultInstance = SelectTextEventBus() 27 | } 28 | } 29 | } 30 | return defaultInstance!! 31 | } 32 | } 33 | 34 | fun register(subscriber: Any, eventClass: Class<*>) { 35 | EventBus.getDefault().register(subscriber) 36 | var subscribedEvents = typesBySubscriber[subscriber] 37 | if (subscribedEvents == null) { 38 | subscribedEvents = ArrayList() 39 | typesBySubscriber[subscriber] = subscribedEvents 40 | } 41 | subscribedEvents.add(eventClass) 42 | } 43 | 44 | @Synchronized 45 | fun isRegistered(subscriber: Any): Boolean { 46 | return if (EventBus.getDefault().isRegistered(subscriber)) { 47 | true 48 | } else typesBySubscriber.containsKey(subscriber) 49 | } 50 | 51 | /** 52 | * 这里主要实现了注销功能 53 | */ 54 | @Synchronized 55 | fun unregister() { 56 | for (key in typesBySubscriber.keys) { 57 | EventBus.getDefault().unregister(key) 58 | } 59 | typesBySubscriber.clear() 60 | } 61 | 62 | /** 63 | * 注销 64 | */ 65 | @Synchronized 66 | fun unregister(subscriber: Any) { 67 | if (typesBySubscriber.containsKey(subscriber)) { 68 | EventBus.getDefault().unregister(subscriber) 69 | typesBySubscriber.remove(subscriber) 70 | } 71 | } 72 | 73 | /** 74 | * 分发事件 75 | * 76 | * @param event 77 | */ 78 | fun dispatch(event: Any?) { 79 | EventBus.getDefault().post(event) 80 | } 81 | 82 | 83 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /selecttext/src/main/java/com/xiaoguang/selecttext/SelectTextPopAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.xiaoguang.selecttext 2 | 3 | import android.content.Context 4 | import android.util.Pair 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.ImageView 9 | import android.widget.LinearLayout 10 | import android.widget.TextView 11 | import androidx.recyclerview.widget.RecyclerView 12 | 13 | /** 14 | * 弹窗 适配器 15 | * hxg 2020.9.13 qq:929842234@qq.com 16 | */ 17 | class SelectTextPopAdapter( 18 | private val mContext: Context, 19 | private val mList: List>? 20 | ) : RecyclerView.Adapter() { 21 | private var itemWrapContent = false 22 | fun setItemWrapContent(itemWrapContent: Boolean) { 23 | this.itemWrapContent = itemWrapContent 24 | } 25 | 26 | private var listener: onClickItemListener? = null 27 | fun setOnclickItemListener(l: onClickItemListener?) { 28 | listener = l 29 | } 30 | 31 | interface onClickItemListener { 32 | fun onClick(position: Int) 33 | } 34 | 35 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 36 | val view = 37 | LayoutInflater.from(mContext).inflate(R.layout.item_select_text_pop, parent, false) 38 | return ViewHolder(view) 39 | } 40 | 41 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 42 | val drawableId = mList!![position].first 43 | val text = mList[position].second 44 | if (itemWrapContent) { 45 | val params = holder.tv_pop_func.layoutParams 46 | params.width = ViewGroup.LayoutParams.WRAP_CONTENT 47 | holder.tv_pop_func.layoutParams = params 48 | holder.tv_pop_func.setPadding(SelectUtils.dp2px(8f), 0, 49 | SelectUtils.dp2px(8f), 0) 50 | } 51 | holder.iv_pop_icon.setBackgroundResource(drawableId) 52 | holder.tv_pop_func.text = text 53 | holder.ll_pop_item.setOnClickListener { listener!!.onClick(position) } 54 | } 55 | 56 | override fun getItemCount(): Int { 57 | return mList?.size ?: 0 58 | } 59 | 60 | inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 61 | val ll_pop_item: LinearLayout 62 | val iv_pop_icon: ImageView 63 | val tv_pop_func: TextView 64 | 65 | init { 66 | ll_pop_item = itemView.findViewById(R.id.ll_pop_item) 67 | iv_pop_icon = itemView.findViewById(R.id.iv_pop_icon) 68 | tv_pop_func = itemView.findViewById(R.id.tv_pop_func) 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | 25 | 26 | 33 | 34 | 39 | 40 | 54 | 55 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/xiaoguang/selecttextview/SelectTextDialog.kt: -------------------------------------------------------------------------------- 1 | package com.xiaoguang.selecttextview 2 | 3 | import android.app.Dialog 4 | import android.content.ClipData 5 | import android.content.ClipboardManager 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.net.Uri 9 | import android.os.Bundle 10 | import android.text.Spannable 11 | import android.text.Spanned 12 | import android.view.Gravity 13 | import android.view.View 14 | import android.view.ViewGroup 15 | import android.widget.TextView 16 | import android.widget.Toast 17 | import androidx.core.content.ContextCompat 18 | import com.xiaoguang.selecttext.SelectTextHelper 19 | import com.zzhoujay.richtext.RichText 20 | 21 | /** 22 | * 选择文字 23 | * hxg 2021/9/14 qq:929842234z 24 | */ 25 | class SelectTextDialog(context: Context?, private val mText: CharSequence) : Dialog( 26 | context!!, R.style.SelectTextFragment 27 | ) { 28 | private lateinit var mSelectableTextHelper: SelectTextHelper 29 | private var selectText: CharSequence? = null 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | setCancelable(true) 33 | setCanceledOnTouchOutside(true) 34 | setContentView(R.layout.dialog_select_text) 35 | 36 | // 一定要在setContentView之后调用,否则无效 37 | window!!.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) 38 | findViewById(R.id.rl_selector).setOnClickListener { v: View? -> 39 | if (mSelectableTextHelper.isPopShowing) { 40 | mSelectableTextHelper.reset() 41 | } else { 42 | dismiss() 43 | } 44 | } 45 | val tv_msg_content = findViewById(R.id.tv_msg_content) 46 | if (mText.isNotEmpty() && (mText.length > 16 || mText.contains("\n"))) { 47 | tv_msg_content.gravity = Gravity.START 48 | } else { 49 | tv_msg_content.gravity = Gravity.CENTER 50 | } 51 | 52 | // 不推荐 富文本可能被修改值 导致gif动不了 53 | if (mText is Spanned || mText is Spannable) { 54 | tv_msg_content.text = mText 55 | setSelectText(tv_msg_content) 56 | } 57 | // 推荐 58 | else { 59 | RichText.initCacheDir(context.applicationContext) 60 | RichText.from(mText.toString()) 61 | .autoFix(false) // 是否自动修复宽高,默认true 62 | .autoPlay(true) // gif自动播放 63 | .done { // 在成功回调处理 64 | // 演示消息列表选择文本 65 | setSelectText(tv_msg_content) 66 | } 67 | .into(tv_msg_content) 68 | } 69 | } 70 | 71 | private fun setSelectText(textView: TextView) { 72 | mSelectableTextHelper = SelectTextHelper.Builder(textView) 73 | .setCursorHandleColor(ContextCompat.getColor(context, R.color.colorAccent)) 74 | .setCursorHandleSizeInDp(24f) 75 | .setSelectedColor(ContextCompat.getColor(context, R.color.colorAccentTransparent)) 76 | .setSelectAll(false) 77 | .addItem(R.drawable.ic_msg_copy, R.string.copy, 78 | object : SelectTextHelper.Builder.onSeparateItemClickListener { 79 | override fun onClick() { 80 | copy(selectText.toString()) 81 | } 82 | }) 83 | .addItem( 84 | R.drawable.ic_msg_select_all, 85 | R.string.select_all, 86 | object : SelectTextHelper.Builder.onSeparateItemClickListener { 87 | override fun onClick() { 88 | selectAll() 89 | } 90 | }) 91 | .addItem(R.drawable.ic_msg_forward, R.string.forward, 92 | object : SelectTextHelper.Builder.onSeparateItemClickListener { 93 | override fun onClick() { 94 | forward(selectText.toString()) 95 | } 96 | }) 97 | .build() 98 | mSelectableTextHelper.setSelectListener(object : SelectTextHelper.OnSelectListenerImpl() { 99 | override fun onClick(v: View?, originalContent: CharSequence?) {} 100 | override fun onLongClick(v: View?) {} 101 | override fun onTextSelected(content: CharSequence?) { 102 | selectText = content 103 | } 104 | 105 | override fun onDismiss() { 106 | dismiss() 107 | } 108 | 109 | override fun onClickUrl(url: String?) { 110 | toast("点击了: $url") 111 | 112 | val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) 113 | context.startActivity(intent) 114 | } 115 | }) 116 | } 117 | 118 | override fun dismiss() { 119 | mSelectableTextHelper.reset() 120 | super.dismiss() 121 | } 122 | 123 | /** 124 | * 复制 125 | */ 126 | private fun copy(selectText: String?) { 127 | SelectTextEventBus.instance.dispatch(SelectTextEvent("dismissAllPop")) 128 | val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 129 | cm.setPrimaryClip(ClipData.newPlainText(selectText, selectText)) 130 | mSelectableTextHelper.reset() 131 | toast("已复制") 132 | } 133 | 134 | /** 135 | * 全选 136 | */ 137 | private fun selectAll() { 138 | SelectTextEventBus.instance.dispatch(SelectTextEvent("dismissAllPop")) 139 | mSelectableTextHelper.selectAll() 140 | } 141 | 142 | /** 143 | * 转发 144 | */ 145 | private fun forward(content: String?) { 146 | SelectTextEventBus.instance.dispatch(SelectTextEvent("dismissAllPop")) 147 | toast("转发") 148 | } 149 | 150 | private fun toast(msg: String) { 151 | Toast.makeText(context.applicationContext, msg, Toast.LENGTH_SHORT).show() 152 | } 153 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /app/src/main/java/com/xiaoguang/selecttextview/CustomPop.kt: -------------------------------------------------------------------------------- 1 | package com.xiaoguang.selecttextview 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import android.graphics.drawable.BitmapDrawable 6 | import android.util.Pair 7 | import android.view.Gravity 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import android.widget.ImageView 12 | import android.widget.PopupWindow 13 | import androidx.annotation.DrawableRes 14 | import androidx.annotation.StringRes 15 | import androidx.recyclerview.widget.GridLayoutManager 16 | import androidx.recyclerview.widget.RecyclerView 17 | import com.xiaoguang.selecttext.SelectTextPopAdapter 18 | import com.xiaoguang.selecttext.SelectTextPopAdapter.onClickItemListener 19 | import com.xiaoguang.selecttext.SelectUtils 20 | import org.greenrobot.eventbus.Subscribe 21 | import org.greenrobot.eventbus.ThreadMode 22 | import java.util.* 23 | 24 | /** 25 | * 聊天长按弹出 26 | * hxg 2019.11.20 27 | */ 28 | class CustomPop( 29 | private val context: Context, 30 | private val msgView: View, 31 | private val isText: Boolean) : PopupWindow() { 32 | 33 | private var rv_content: RecyclerView? = null 34 | private var iv_arrow_up: ImageView? = null 35 | private var iv_arrow: ImageView? = null 36 | private val itemTextList: MutableList> = LinkedList() 37 | private val itemListenerList: MutableList = LinkedList() 38 | private var listAdapter: SelectTextPopAdapter? = null 39 | private var popupWindow: PopupWindow? = null 40 | private var mWidth = 0 // 本pop的宽 41 | private var mHeight = 0 // 本pop的高 42 | 43 | /** 44 | * 图标 和 文字 45 | */ 46 | fun addItem( 47 | @DrawableRes drawableId: Int, 48 | @StringRes textResId: Int, 49 | listener: onSeparateItemClickListener 50 | ) { 51 | addItem(drawableId, context.getString(textResId), listener) 52 | } 53 | 54 | /** 55 | * 图标 和 文字 56 | */ 57 | fun addItem( 58 | @DrawableRes drawableId: Int, 59 | itemText: String, 60 | listener: onSeparateItemClickListener 61 | ) { 62 | itemTextList.add(Pair(drawableId, itemText)) 63 | itemListenerList.add(listener) 64 | } 65 | 66 | /** 67 | * 只有文字 68 | */ 69 | fun addItem(itemText: String, listener: onSeparateItemClickListener) { 70 | addItem(0, itemText, listener) 71 | } 72 | 73 | /** 74 | * 只有文字 75 | */ 76 | fun addItem(@StringRes textResId: Int, listener: onSeparateItemClickListener) { 77 | addItem(context.getString(textResId), listener) 78 | } 79 | 80 | /** 81 | * 设置背景 和 箭头 82 | */ 83 | fun setPopStyle(bgColor: Int, arrowImg: Int) { 84 | if (null != rv_content && null != iv_arrow) { 85 | rv_content!!.setBackgroundResource(bgColor) 86 | iv_arrow!!.setBackgroundResource(arrowImg) 87 | SelectUtils.Companion.setWidthHeight(iv_arrow!!, dp2px(14), dp2px(7)) 88 | } 89 | } 90 | 91 | /** 92 | * 设置每个item自适应 93 | */ 94 | fun setItemWrapContent() { 95 | if (null != listAdapter) { 96 | listAdapter!!.setItemWrapContent(true) 97 | } 98 | } 99 | 100 | fun show() { 101 | if (null != itemTextList && itemTextList.size <= 0) { 102 | return 103 | } 104 | updateListView() 105 | } 106 | 107 | interface onSeparateItemClickListener { 108 | fun onClick() 109 | } 110 | 111 | /** 112 | * public end 113 | */ 114 | private fun init() { 115 | listAdapter = SelectTextPopAdapter(context, itemTextList) 116 | listAdapter!!.setOnclickItemListener(object : onClickItemListener { 117 | override fun onClick(position: Int) { 118 | SelectTextEventBus.instance.dispatch(SelectTextEvent("dismissAllPop")) 119 | dismiss() 120 | itemListenerList[position].onClick() 121 | } 122 | }) 123 | val popWindowView = LayoutInflater.from(context).inflate(R.layout.pop_operate, null) 124 | rv_content = popWindowView.findViewById(R.id.rv_content) 125 | iv_arrow_up = popWindowView.findViewById(R.id.iv_arrow_up) 126 | iv_arrow = popWindowView.findViewById(R.id.iv_arrow) 127 | if (isText) { 128 | popupWindow = PopupWindow( 129 | popWindowView, 130 | ViewGroup.LayoutParams.WRAP_CONTENT, 131 | ViewGroup.LayoutParams.WRAP_CONTENT, 132 | false 133 | ) 134 | popupWindow!!.isClippingEnabled = false 135 | if (!SelectTextEventBus.instance.isRegistered(this)) { 136 | SelectTextEventBus.instance.register(this, SelectTextEvent::class.java) 137 | } 138 | } else { 139 | popupWindow = PopupWindow( 140 | popWindowView, 141 | ViewGroup.LayoutParams.WRAP_CONTENT, 142 | ViewGroup.LayoutParams.WRAP_CONTENT, 143 | true 144 | ) 145 | // 使其聚集 146 | popupWindow!!.isFocusable = true 147 | // 设置允许在外点击消失 148 | popupWindow!!.isOutsideTouchable = true 149 | // 这个是为了点击“返回Back”也能使其消失,并且并不会影响你的背景 150 | popupWindow!!.setBackgroundDrawable(BitmapDrawable()) 151 | } 152 | // 动画 153 | popupWindow?.animationStyle = R.style.Base_Animation_AppCompat_Dialog 154 | } 155 | 156 | private fun updateListView() { 157 | listAdapter!!.notifyDataSetChanged() 158 | if (rv_content != null) { 159 | rv_content!!.adapter = listAdapter 160 | } 161 | val size = itemTextList.size 162 | val deviceWidth = SelectUtils.Companion.displayWidth 163 | val deviceHeight = SelectUtils.Companion.displayHeight 164 | val statusHeight = SelectUtils.Companion.statusHeight 165 | //计算箭头显示的位置 166 | val location = IntArray(2) 167 | msgView.getLocationOnScreen(location) 168 | val msgViewWidth = msgView.width 169 | val msgViewHeight = msgView.height 170 | // view中心坐标 = view的位置 + view的宽度 / 2 171 | val centerWidth = location[0] + msgViewWidth / 2 172 | if (size > 5) { 173 | mWidth = dp2px(12 * 4 + 52 * 5) 174 | mHeight = dp2px(12 * 3 + 52 * 2 + 5) 175 | } else { 176 | mWidth = dp2px(12 * 4 + 52 * size) 177 | mHeight = dp2px(12 * 2 + 52 + 5) 178 | } 179 | // topUI true pop显示在顶部 180 | val topUI = location[1] > mHeight + statusHeight 181 | val arrowView: View? 182 | if (topUI) { 183 | iv_arrow!!.visibility = View.VISIBLE 184 | iv_arrow_up!!.visibility = View.GONE 185 | arrowView = iv_arrow 186 | } else { 187 | iv_arrow_up!!.visibility = View.VISIBLE 188 | iv_arrow!!.visibility = View.GONE 189 | arrowView = iv_arrow_up 190 | } 191 | if (size > 5) { 192 | rv_content!!.layoutManager = 193 | GridLayoutManager(context, 5, GridLayoutManager.VERTICAL, false) 194 | // x轴 (屏幕 - mWidth)/ 2 195 | val posX = (deviceWidth - mWidth) / 2 196 | // topUI ? 197 | // msgView的y轴 - popupWindow的高度 198 | // :msgView的y轴 + msgView高度 + 8dp间距 199 | var posY = if (topUI) location[1] - mHeight else location[1] + msgViewHeight + dp2px(8) 200 | if (!topUI // 反向的ui 201 | // 底部已经超过了 屏幕高度 - (弹窗高度 + 输入框) 202 | && location[1] + msgView.height > deviceHeight - dp2px(52 * 2 + 60) 203 | ) { 204 | // 显示在屏幕3/4高度 205 | posY = deviceHeight * 3 / 4 206 | } 207 | popupWindow!!.showAtLocation(msgView, Gravity.NO_GRAVITY, posX, posY) 208 | val arrX = mWidth / 2 - dp2px(12 + 4) 209 | arrowView!!.translationX = arrX.toFloat() 210 | } else { 211 | rv_content!!.layoutManager = 212 | GridLayoutManager(context, size, GridLayoutManager.VERTICAL, false) 213 | // x轴 (屏幕 - mWidth)/ 2 214 | var posX = centerWidth - mWidth / 2 215 | // 右侧的最大宽度 216 | val max = centerWidth + mWidth / 2 217 | if (posX < 0) { 218 | posX = 0 219 | } else if (max > deviceWidth) { 220 | posX = deviceWidth - mWidth 221 | } 222 | // topUI ? 223 | // msgView的y轴 - popupWindow的高度 224 | // :msgView的y轴 + msgView高度 + 8dp间距 225 | var posY = if (topUI) location[1] - mHeight else location[1] + msgViewHeight + dp2px(8) 226 | if (!topUI // 反向的ui 227 | // 底部已经超过了 屏幕高度 - (弹窗高度 + 输入框) 228 | && location[1] + msgView.height > deviceHeight - dp2px(52 * 2 + 60) 229 | ) { 230 | // 显示在屏幕3/4高度 231 | posY = deviceHeight * 3 / 4 232 | } 233 | popupWindow!!.showAtLocation(msgView, Gravity.NO_GRAVITY, posX, posY) 234 | // view中心坐标 - pop坐标 - 16dp padding 235 | val arrX = centerWidth - posX - dp2px(16) 236 | arrowView!!.translationX = arrX.toFloat() 237 | } 238 | } 239 | 240 | // 隐藏 弹窗 241 | @Subscribe(threadMode = ThreadMode.MAIN) 242 | fun handleSelector(event: SelectTextEvent) { 243 | // 隐藏操作弹窗 244 | if ("dismissOperatePop" == event.type) { 245 | dismiss() 246 | } 247 | } 248 | 249 | override fun dismiss() { 250 | popupWindow!!.dismiss() 251 | SelectTextEventBus.instance.unregister(this) 252 | } 253 | 254 | companion object { 255 | private fun dp2px(num: Int): Int { 256 | return (num * Resources.getSystem().displayMetrics.density + 0.5f).toInt() 257 | } 258 | } 259 | 260 | /** 261 | * public start 262 | */ 263 | init { 264 | init() 265 | } 266 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SelectTextHelper-高仿微信聊天消息列表自由复制文字,双击查看文本内容 2 | 3 | ## [掘金地址](https://juejin.cn/post/7008080194116255752) [github地址](https://github.com/ITxiaoguang/SelectTextHelper) 4 | 5 | `SelectTextHelper`打造一个全网最逼近微信聊天消息自由复制,双击查看文本内容框架。 支持图片和富文本选中,汇聚底层`TextView`框架、原理并加以整理得出的一个实用的`Helper`。 6 | 仅用几个类实现便实现如此强大的功能,用法也超级简单,侵入性极低。 7 | 8 | 项目持续维护中... 9 | 您的宝贵意见和建议是技术前进的方向 10 | 11 | [![](https://jitpack.io/v/ITxiaoguang/SelectTextHelper.svg)](https://jitpack.io/#ITxiaoguang/SelectTextHelper) 12 | 13 | 14 | ## 项目演示 15 | 16 | | 消息页效果 | 查看内容效果 | 17 | | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------: | 18 | | ![img\_v2\_c8287d37-8b53-43b0-abc9-6788d73f50dg.jpg](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1121f21620a74c13b8353cafcde0df19~tplv-k3u1fbpfcp-watermark.image?) | ![img_v2_0256892e-5ef8-4610-b73f-7ef9e4a9668g.jpg](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ddeab10820e54b9f84e0220a959b990c~tplv-k3u1fbpfcp-watermark.image?) | 19 | 20 | | 消息页全选 | 消息页自由复制放大镜 | 21 | | :---------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------: | 22 | | ![demo\_1.jpg](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/99068250b93644d4921c3afe4ef39dbc~tplv-k3u1fbpfcp-watermark.image?) | ![demo\_2.jpg](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c800612998d4415e95c23a201f99c669~tplv-k3u1fbpfcp-watermark.image?) | 23 | 24 | | 消息页选中文本 | 查看内容 | 25 | | :---------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------: | 26 | | ![demo\_3.jpg](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b9567b55e9384dcebf394202a9ec849c~tplv-k3u1fbpfcp-watermark.image?) | ![demo\_4.jpg](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7cd289a09a2142d2a50232e717cbd179~tplv-k3u1fbpfcp-watermark.image?) | 27 | 28 | ## 特点功能: 29 | 30 | * 支持自由选择文本 31 | * 支持`富文本`选择 32 | * 支持自定义文本有:游标颜色、游标大小、选中文本颜色 33 | * 支持默认全选文字或选2个文字 34 | * 支持滑动依然显示弹窗 35 | * 支持放大镜功能 36 | * 支持全选情况下自定义弹窗 37 | * 支持操作弹窗:每行个数、图片、文字、监听回调、弹窗颜色、箭头图片 38 | 39 | ## Demo 40 | 41 | ![demo.jpg](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d52501acc3c44315aa5731772b007b24~tplv-k3u1fbpfcp-zoom-1.image) 42 | 43 | ## 如何添加 44 | 45 | ### Gradle添加: 46 | 47 | #### 1.在Project的`build.gradle`中添加仓库地址 48 | 49 | ```gradle 50 | allprojects { 51 | repositories { 52 | ... 53 | maven { url "https://jitpack.io" } 54 | } 55 | } 56 | ``` 57 | 58 | #### 2.在Module目录下的`build.gradle`中添加依赖 59 | 60 | [![](https://jitpack.io/v/ITxiaoguang/SelectTextHelper.svg)](https://jitpack.io/#ITxiaoguang/SelectTextHelper) 61 | 62 | 63 | ```gradle 64 | dependencies { 65 | implementation 'com.github.ITxiaoguang:SelectTextHelper:1.1.0' 66 | } 67 | ``` 68 | 69 | ## 传送门 70 | 71 | * [仿照的例子](https://www.dazhuanlan.com/t0915/topics/1440960) 72 | * [放大镜](https://developer.android.google.cn/guide/topics/text/magnifier) 73 | * [TextView](https://developer.android.google.cn/reference/android/widget/TextView) 74 | * [富文本](https://github.com/zzhoujay/RichText) 75 | 76 | 77 | ### 主要实现 78 | 79 | 通过 [仿照的例子](https://www.dazhuanlan.com/t0915/topics/1440960) 并改进弹窗坐标位置、加对ImageSpan支持、大小加上`EventBus`实现 80 | 81 | ### 简单用例 82 | 83 | #### 1.导入代码 84 | 85 | 把该项目里的`selecttext Module`放入你的项目里面 或者 按照`Gradle`添加的步骤导入依赖。 86 | 87 | #### 2.给你的`TextView`创建`Helper`和加监听 88 | 89 | ```kotlin 90 | val mSelectableTextHelper = SelectTextHelper.Builder(textView) // 放你的textView到这里!! 91 | .setCursorHandleColor(ContextCompat.getColor(mContext, R.color.colorAccent)) // 游标颜色 92 | .setCursorHandleSizeInDp(22f) // 游标大小 单位dp 93 | .setSelectedColor(ContextCompat.getColor(mContext, R.color.colorAccentTransparent)) // 选中文本的颜色 94 | .setSelectAll(true) // 初次选中是否全选 default true 95 | .setScrollShow(true) // 滚动时是否继续显示 default true 96 | .setSelectedAllNoPop(true) // 已经全选无弹窗,设置了监听会回调 onSelectAllShowCustomPop 方法 97 | .setMagnifierShow(true) // 放大镜 default true 98 | .setSelectTextLength(2)// 首次选中文本的长度 default 2 99 | .setPopDelay(100)// 弹窗延迟时间 default 100毫秒 100 | .setPopAnimationStyle(R.style.Base_Animation_AppCompat_Dialog)// 弹窗动画 default 无动画 101 | .addItem(0/*item的图标*/,"复制"/*item的描述*/, {Log.i("SelectTextHelper","复制")/*item的回调*/}// 操作弹窗的每个item 102 | .setPopSpanCount(5) // 设置操作弹窗每行个数 default 5 103 | .setPopStyle( 104 | R.drawable.shape_color_4c4c4c_radius_8 /*操作弹窗背*/, 105 | R.drawable.ic_arrow /*箭头图片*/ 106 | ) // 设置操作弹窗背景色、箭头图片 107 | .build() 108 | 109 | mSelectableTextHelper!!.setSelectListener(object : OnSelectListener { 110 | /** 111 | * 点击回调 112 | */ 113 | override fun onClick(v: View?, originalContent: CharSequence?) { 114 | // 拿原始文本方式 115 | // clickTextView(msgBean.content!!) // 推荐 116 | // clickTextView(originalContent!!) // 不推荐 富文本可能被修改值 导致gif动不了 117 | } 118 | 119 | /** 120 | * 长按回调 121 | */ 122 | override fun onLongClick(v: View?) { 123 | } 124 | 125 | /** 126 | * 选中文本回调 127 | */ 128 | override fun onTextSelected(content: CharSequence?) { 129 | } 130 | 131 | /** 132 | * 弹窗关闭回调 133 | */ 134 | override fun onDismiss() {} 135 | 136 | /** 137 | * 点击TextView里的url回调 138 | * 139 | * 已被下面重写 140 | * textView.setMovementMethod(new LinkMovementMethodInterceptor()); 141 | */ 142 | override fun onClickUrl(url: String?) { 143 | } 144 | 145 | /** 146 | * 全选显示自定义弹窗回调 147 | */ 148 | override fun onSelectAllShowCustomPop() { 149 | } 150 | 151 | /** 152 | * 重置回调 153 | */ 154 | override fun onReset() { 155 | // SelectTextEventBus.instance.dispatch(SelectTextEvent("dismissOperatePop")) 156 | } 157 | 158 | /** 159 | * 解除自定义弹窗回调 160 | */ 161 | override fun onDismissCustomPop() { 162 | // SelectTextEventBus.instance.dispatch(SelectTextEvent("dismissOperatePop")) 163 | } 164 | 165 | /** 166 | * 是否正在滚动回调 167 | */ 168 | override fun onScrolling() { 169 | // removeShowSelectView() 170 | } 171 | }) 172 | 173 | ``` 174 | 175 | #### 3.demo中提供了查看文本内容的`SelectTextDialog`和 消息列表自由复制`MainActivity` 176 | 177 | 查看文本内容方法: 178 | 179 | * 该方法比较简单,将`textView`参照步骤2放入`SelectTextHelper`中,在`dismiss`调用`SelectTextHelper`的`reset()`即可。 180 | 181 | ```kotlin 182 | override fun dismiss() { 183 | mSelectableTextHelper.reset() 184 | super.dismiss() 185 | } 186 | ``` 187 | 188 | 高仿微信聊天消息列表自由复制方法: 189 | 190 | * `recycleView` + `adapter` + 多布局的使用在这里不阐述,请看本项目demo。 191 | 192 | * 为`adapter`里text类型`ViewHolder`中的`textView`参照步骤2放入`SelectTextHelper`中,注册`SelectTextEventBus`。 193 | 194 | * `SelectTextEventBus`类特别说明、原理: 195 | `SelectTextEventBus`在`EventBus`基础上加功能。在`register`时记录下类和方法,方便在`Activity/Fragment Destroy`时`unregister`所有`SelectTextEventBus`的`EventBus`。 196 | 197 | * text类型`ViewHolder` 添加`EventBus`监听 198 | 199 | ```kotlin 200 | /** 201 | * 自定义SelectTextEvent 隐藏 光标 202 | */ 203 | @Subscribe(threadMode = ThreadMode.MAIN) 204 | fun handleSelector(event: SelectTextEvent) { 205 | if (null == mSelectableTextHelper) { 206 | return 207 | } 208 | val type = event.type 209 | if (TextUtils.isEmpty(type)) { 210 | return 211 | } 212 | when (type) { 213 | "dismissAllPop" -> mSelectableTextHelper!!.reset() 214 | "dismissAllPopDelayed" -> postReset(Companion.RESET_DELAY) 215 | } 216 | } 217 | ``` 218 | 219 | * 重写`adapter`里的`onViewRecycled`方法,该方法在回收`View`时调用 220 | 221 | ```kotlin 222 | override fun onViewRecycled(holder: RecyclerView.ViewHolder) { 223 | super.onViewRecycled(holder) 224 | if (holder is ViewHolderText) { 225 | // 注销 226 | SelectTextEventBus.instance.unregister(holder) 227 | } 228 | } 229 | ``` 230 | 231 | - 对ImageSpan表情支持(支持动态表情!!)(https://github.com/ITxiaoguang/SelectTextHelper/issues/4 ) 232 | 233 | ```kotlin 234 | val emojiMap: MutableMap = HashMap() 235 | emojiMap["\\[笑脸\\]"] = R.drawable.emoji_00 236 | emojiMap["\\[瘪嘴\\]"] = R.drawable.emoji_01 237 | emojiMap["\\[色\\]"] = R.drawable.emoji_02 238 | emojiMap["\\[瞪大眼\\]"] = R.drawable.emoji_03 239 | emojiMap["\\[酷\\]"] = R.drawable.emoji_04 240 | emojiMap["\\[Android\\]"] = R.mipmap.ic_launcher_round 241 | emojiMap["\\[好的\\]"] = R.drawable.emoji_gif 242 | emojiMap["\\[羊驼\\]"] = R.drawable.emoji_gif2 243 | ```` 244 | 245 | - 富文本支持 [富文本用法点这里](https://github.com/zzhoujay/RichText) 246 | 247 | ```kotlin 248 | // todo 方法一:富文本 需要转行成富文本形式 249 | RichText.initCacheDir(holder.textView.context.applicationContext) // 项目里初始化一次即可 250 | RichText.from(msgBean.content) 251 | .autoFix(false) // 是否自动修复宽高,默认true 252 | .autoPlay(true) // gif自动播放 253 | .singleLoad(false) // RecyclerView里设为false 若同时启动了多个RichText,会并发解析,类似于AsyncTask的executeOnExecutor 254 | .done { // 在成功回调处理 255 | // 演示消息列表选择文本 256 | holder.selectText(msgBean) 257 | } 258 | .into(holder.textView) 259 | 260 | // todo 方法二:普通文本 261 | holder.textView.text = msgBean.content 262 | // 演示消息列表选择文本 263 | holder.selectText(msgBean) 264 | ``` 265 | -------------------------------------------------------------------------------- /selecttext/src/main/java/com/xiaoguang/selecttext/SelectUtils.kt: -------------------------------------------------------------------------------- 1 | package com.xiaoguang.selecttext 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import android.graphics.Color 6 | import android.graphics.drawable.AnimatedImageDrawable 7 | import android.graphics.drawable.AnimatedVectorDrawable 8 | import android.graphics.drawable.AnimationDrawable 9 | import android.os.Build 10 | import android.text.Layout 11 | import android.text.Spannable 12 | import android.text.SpannableStringBuilder 13 | import android.text.TextUtils 14 | import android.text.style.DynamicDrawableSpan 15 | import android.view.View 16 | import android.widget.TextView 17 | import androidx.core.content.ContextCompat 18 | import java.util.regex.Pattern 19 | 20 | /** 21 | * 弹窗 适配器 22 | * hxg 2023.1.5 qq:929842234@qq.com 23 | */ 24 | class SelectUtils { 25 | 26 | companion object { 27 | 28 | /** 29 | * 替换内容 30 | * 31 | * @param stringBuilder SpannableStringBuilder text 32 | * @param mOriginalContent CharSequence text 33 | * @param targetText Target Text 34 | * @param replaceText Replace Text 35 | */ 36 | fun replaceContent( 37 | stringBuilder: SpannableStringBuilder, 38 | mOriginalContent: CharSequence, 39 | targetText: String, 40 | replaceText: String, 41 | ) { 42 | val startIndex = mOriginalContent.toString().indexOf(targetText) 43 | if (-1 != startIndex) { 44 | val endIndex = startIndex + targetText.length 45 | stringBuilder.replace(startIndex, endIndex, replaceText) 46 | } 47 | } 48 | 49 | /** 50 | * 文字转化成图片背景 51 | * 52 | * @param context Context 53 | * @param stringBuilder SpannableStringBuilder text 54 | * @param content Target content 55 | */ 56 | fun replaceText2Emoji( 57 | context: Context?, 58 | emojiMap: MutableMap, 59 | stringBuilder: SpannableStringBuilder, 60 | content: CharSequence 61 | ) { 62 | if (emojiMap.isEmpty()) { 63 | return 64 | } 65 | for ((key, drawableRes) in emojiMap) { 66 | val matcher = Pattern.compile(key).matcher(content) 67 | while (matcher.find()) { 68 | val start = matcher.start() 69 | val end = matcher.end() 70 | val drawable = ContextCompat.getDrawable(context!!, drawableRes) 71 | // 动画图(加载多张 Drawable 图片资源组合而成的动画) 72 | if (drawable is AnimationDrawable) { 73 | drawable.start() // 开始播放动画 74 | } 75 | // 动态图 76 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 77 | if (drawable is AnimatedImageDrawable) { 78 | drawable.start() 79 | } 80 | } 81 | // 动态矢量图 82 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 83 | if (drawable is AnimatedVectorDrawable) { 84 | drawable.start() 85 | } 86 | } 87 | drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) 88 | val span = SelectImageSpan( 89 | drawable, Color.TRANSPARENT, DynamicDrawableSpan.ALIGN_CENTER 90 | ) 91 | stringBuilder.setSpan(span, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) 92 | } 93 | } 94 | } 95 | 96 | fun getPreciseOffset(textView: TextView, x: Int, y: Int): Int { 97 | val layout = textView.layout 98 | return if (layout != null) { 99 | val topVisibleLine = layout.getLineForVertical(y) 100 | val offset = layout.getOffsetForHorizontal(topVisibleLine, x.toFloat()) 101 | val offsetX = layout.getPrimaryHorizontal(offset).toInt() 102 | if (offsetX > x) { 103 | layout.getOffsetToLeftOf(offset) 104 | } else { 105 | offset 106 | } 107 | } else { 108 | -1 109 | } 110 | } 111 | 112 | fun getHysteresisOffset(textView: TextView, x: Int, y: Int, previousOffset: Int): Int { 113 | var previousOffsetCopy = previousOffset 114 | val layout = textView.layout ?: return -1 115 | var line = layout.getLineForVertical(y) 116 | 117 | // The "HACK BLOCK"S in this function is required because of how Android Layout for 118 | // TextView works - if 'offset' equals to the last character of a line, then 119 | // 120 | // * getLineForOffset(offset) will result the NEXT line 121 | // * getPrimaryHorizontal(offset) will return 0 because the next insertion point is on the next line 122 | // * getOffsetForHorizontal(line, x) will not return the last offset of a line no matter where x is 123 | // These are highly undesired and is worked around with the HACK BLOCK 124 | // 125 | // @see Moon+ Reader/Color Note - see how it can't select the last character of a line unless you move 126 | // the cursor to the beginning of the next line. 127 | // 128 | ////////////////////HACK BLOCK//////////////////////////////////////////////////// 129 | if (isEndOfLineOffset(layout, previousOffsetCopy)) { // we have to minus one from the offset so that the code below to find 130 | // the previous line can work correctly. 131 | val left = layout.getPrimaryHorizontal(previousOffsetCopy - 1).toInt() 132 | val right = layout.getLineRight(line).toInt() 133 | val threshold = (right - left) / 2 // half the width of the last character 134 | if (x > right - threshold) { 135 | previousOffsetCopy -= 1 136 | } 137 | } /////////////////////////////////////////////////////////////////////////////////// 138 | val previousLine = layout.getLineForOffset(previousOffsetCopy) 139 | val previousLineTop = layout.getLineTop(previousLine) 140 | val previousLineBottom = layout.getLineBottom(previousLine) 141 | val hysteresisThreshold = (previousLineBottom - previousLineTop) / 2 142 | 143 | // If new line is just before or after previous line and y position is less than 144 | // hysteresisThreshold away from previous line, keep cursor on previous line. 145 | if (line == previousLine + 1 && y - previousLineBottom < hysteresisThreshold || line == previousLine - 1 && (previousLineTop - y) < hysteresisThreshold) { 146 | line = previousLine 147 | } 148 | var offset = layout.getOffsetForHorizontal(line, x.toFloat()) 149 | 150 | // This allow the user to select the last character of a line without moving the 151 | // cursor to the next line. (As Layout.getOffsetForHorizontal does not return the 152 | // offset of the last character of the specified line) 153 | // 154 | // But this function will probably get called again immediately, must decrement the offset 155 | // by 1 to compensate for the change made below. (see previous HACK BLOCK) 156 | /////////////////////HACK BLOCK/////////////////////////////////////////////////// 157 | if (offset < textView.text.length - 1) { 158 | val right = layout.getLineRight(line).toInt() 159 | val isEnd = x >= right // 是否选到了最后 160 | // FIX 这里的 offset + 1 不一定是对的,需要判断最后一个字符长度, 161 | if (isEnd) { 162 | val left = layout.getPrimaryHorizontal(offset).toInt() 163 | val right = layout.getLineRight(line).toInt() 164 | val threshold = (right - left) / 2 // half the width of the last character 165 | if (x > right - threshold) { 166 | val index = getLastTextLength(layout, offset) // 得到最后一个字符长度 167 | offset += index // offset + 最后一个字符长度 168 | } 169 | } 170 | } ////////////////////////////////////////////////////////////////////////////////// 171 | return offset 172 | } 173 | 174 | /** 175 | * 得到最后一个字符长度 176 | */ 177 | private fun getLastTextLength(layout: Layout, offset: Int): Int { 178 | var index = 1 // 得到最后一个字符长度 179 | val num = 1..20 180 | for (i in num) { 181 | if (isEndOfLineOffset(layout, offset + i)) { 182 | index = i 183 | break 184 | } 185 | } 186 | return index 187 | } 188 | 189 | private fun isEndOfLineOffset(layout: Layout, offset: Int): Boolean { 190 | return offset > 0 && layout.getLineForOffset(offset) == layout.getLineForOffset(offset - 1) + 1 191 | } 192 | 193 | val displayWidth: Int 194 | get() = Resources.getSystem().displayMetrics.widthPixels 195 | 196 | val displayHeight: Int 197 | get() = Resources.getSystem().displayMetrics.heightPixels 198 | 199 | fun dp2px(dpValue: Float): Int { 200 | return (dpValue * Resources.getSystem().displayMetrics.density + 0.5f).toInt() 201 | } 202 | 203 | /** 204 | * 设置宽高 205 | * 206 | * @param v 207 | * @param w 208 | * @param h 209 | */ 210 | fun setWidthHeight(v: View, w: Int, h: Int) { 211 | val params = v.layoutParams 212 | params.width = w 213 | params.height = h 214 | v.layoutParams = params 215 | } 216 | 217 | /** 218 | * 通知栏的高度 219 | */ 220 | private var STATUS_HEIGHT = 0 221 | 222 | /** 223 | * 获取通知栏的高度 224 | */ 225 | val statusHeight: Int 226 | get() { 227 | if (0 != STATUS_HEIGHT) { 228 | return STATUS_HEIGHT 229 | } 230 | val resid = 231 | Resources.getSystem().getIdentifier("status_bar_height", "dimen", "android") 232 | if (resid > 0) { 233 | STATUS_HEIGHT = Resources.getSystem().getDimensionPixelSize(resid) 234 | return STATUS_HEIGHT 235 | } 236 | return -1 237 | } 238 | 239 | /** 240 | * 反射获取对象属性值 241 | */ 242 | fun getFieldValue(obj: Any?, fieldName: String?): Any? { 243 | if (obj == null || TextUtils.isEmpty(fieldName)) { 244 | return null 245 | } 246 | var clazz: Class<*> = obj.javaClass 247 | while (clazz != Any::class.java) { 248 | try { 249 | val field = clazz.getDeclaredField(fieldName!!) 250 | field.isAccessible = true 251 | return field[obj] 252 | } catch (ignore: Exception) { 253 | } 254 | clazz = clazz.superclass 255 | } 256 | return null 257 | } 258 | 259 | /** 260 | * 判断是否为emoji表情符 261 | * 262 | * @param c 字符 263 | * @return 是否为emoji字符 264 | */ 265 | fun isEmojiText(c: Char): Boolean { 266 | return !(c.code == 0x0 || c.code == 0x9 || c.code == 0xA || c.code == 0xD || c.code in 0x20..0xD7FF || c.code in 0xE000..0xFFFD || c.code in 0x100000..0x10FFFF) 267 | } 268 | 269 | /** 270 | * 利用反射检测文本是否是ImageSpan文本 271 | */ 272 | fun isImageSpanText(mSpannable: Spannable): Boolean { 273 | if (TextUtils.isEmpty(mSpannable)) { 274 | return false 275 | } 276 | try { 277 | val mSpans = getFieldValue(mSpannable, "mSpans") as Array<*>? 278 | if (null != mSpans) { 279 | for (mSpan in mSpans) { 280 | if (mSpan is SelectImageSpan) { 281 | return true 282 | } 283 | } 284 | } 285 | } catch (ignore: Exception) { 286 | } 287 | return false 288 | } 289 | 290 | /** 291 | * 匹配Image 292 | * 293 | * @param emojiMap Emoji picture 294 | * @param content Target content 295 | */ 296 | fun matchImageSpan(emojiMap: MutableMap, content: String): Boolean { 297 | if (emojiMap.isEmpty()) { 298 | return false 299 | } 300 | for ((key) in emojiMap) { 301 | val matcher = Pattern.compile(key).matcher(content) 302 | if (matcher.find()) { 303 | return true 304 | } 305 | } 306 | return false 307 | } 308 | } 309 | 310 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xiaoguang/selecttextview/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.xiaoguang.selecttextview 2 | 3 | import android.os.Bundle 4 | import android.view.MotionEvent 5 | import android.widget.EditText 6 | import android.widget.TextView 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.recyclerview.widget.LinearLayoutManager 9 | import androidx.recyclerview.widget.RecyclerView 10 | import com.xiaoguang.selecttext.SelectTextHelper 11 | 12 | class MainActivity : AppCompatActivity() { 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | setContentView(R.layout.activity_main) 16 | 17 | // 绑定表情 方法一: 18 | // SelectTextHelper.putEmojiMap("\\[笑脸\\]", R.drawable.emoji_00) 19 | // SelectTextHelper.putEmojiMap("\\[瘪嘴\\]", R.drawable.emoji_01) 20 | // SelectTextHelper.putEmojiMap("\\[色\\]", R.drawable.emoji_02) 21 | // SelectTextHelper.putEmojiMap("\\[瞪大眼\\]", R.drawable.emoji_03) 22 | // SelectTextHelper.putEmojiMap("\\[酷\\]", R.drawable.emoji_04) 23 | // 绑定表情 方法二: 24 | val emojiMap: MutableMap = HashMap() 25 | emojiMap["\\[笑脸\\]"] = R.drawable.emoji_00 26 | emojiMap["\\[瘪嘴\\]"] = R.drawable.emoji_01 27 | emojiMap["\\[色\\]"] = R.drawable.emoji_02 28 | emojiMap["\\[瞪大眼\\]"] = R.drawable.emoji_03 29 | emojiMap["\\[酷\\]"] = R.drawable.emoji_04 30 | emojiMap["\\[Android\\]"] = R.mipmap.ic_launcher_round 31 | emojiMap["\\[好的\\]"] = R.drawable.emoji_gif 32 | emojiMap["\\[羊驼\\]"] = R.drawable.emoji_gif2 33 | SelectTextHelper.putAllEmojiMap(emojiMap) 34 | 35 | // todo 一:展示在列表中自由复制、双击查看文本 36 | val rvMsg = findViewById(R.id.rv_msg) 37 | rvMsg.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) 38 | val mList: MutableList = ArrayList() 39 | addList(mList) 40 | val adapter = MsgAdapter(this, mList) 41 | rvMsg.adapter = adapter 42 | initInput(rvMsg, adapter, mList) 43 | 44 | // todo 二:展示查看文本 45 | // val dialog = SelectTextDialog(this, TEXT6) 46 | // dialog.show() 47 | 48 | } 49 | 50 | private fun initInput( 51 | rvMsg: RecyclerView, 52 | adapter: MsgAdapter, 53 | mList: MutableList 54 | ) { 55 | val etInput = findViewById(R.id.et_input) 56 | val tvSend = findViewById(R.id.tv_send) 57 | tvSend.setOnClickListener { 58 | rvMsg.scrollToPosition(adapter.itemCount - 1) 59 | rvMsg.postDelayed({ 60 | val input = etInput.text.toString() 61 | mList.add(MsgBean(1, false, input)) 62 | etInput.setText("") 63 | adapter.notifyItemChanged(adapter.itemCount - 1) 64 | rvMsg.scrollToPosition(adapter.itemCount - 1) 65 | }, 16) 66 | } 67 | } 68 | 69 | override fun onDestroy() { 70 | super.onDestroy() 71 | SelectTextEventBus.instance.unregister() 72 | } 73 | 74 | override fun dispatchTouchEvent(ev: MotionEvent): Boolean { 75 | if (ev.action == MotionEvent.ACTION_DOWN) { 76 | SelectTextEventBus.instance.dispatch(SelectTextEvent("dismissAllPopDelayed")) 77 | } 78 | return super.dispatchTouchEvent(ev) 79 | } 80 | 81 | // 1:文本、2:图片、3:链接 82 | private fun addList(mList: MutableList) { 83 | mList.add(MsgBean(1, true, TEXT0)) 84 | mList.add(MsgBean(1, true, TEXT1)) 85 | mList.add(MsgBean(2, false, IMG1)) 86 | // mList.add(MsgBean(3, true, LINK1)) 87 | mList.add(MsgBean(1, false, TEXT2)) 88 | mList.add(MsgBean(1, true, TEXT3)) 89 | mList.add(MsgBean(1, false, TEXT4)) 90 | mList.add(MsgBean(1, true, TEXT5)) 91 | mList.add(MsgBean(1, false, TEXT6)) 92 | mList.add(MsgBean(1, true, TEXT7)) 93 | mList.add(MsgBean(1, false, TEXT8)) 94 | mList.add(MsgBean(1, true, TEXT9)) 95 | } 96 | 97 | companion object { 98 | var TEXT0 = 99 | "各种文本演示:\n这些都是富文本: 链接 https://github.com/ITxiaoguang/SelectTextHelper Emoji表情:\uD83D\uDE04\uD83D\uDE03 自定义本地静态表情:[Android][笑脸] 自定义本地动态表情:[好的] 网络静态图和网络动态图:

" + 100 | "

\"点赞\"

" + 101 | "

\"动态图\"

" + 102 | "下demo来看你就会了" 103 | var TEXT1 = "纯文本" 104 | var TEXT2 = "文本是链接 https://github.com/ITxiaoguang/SelectTextHelper\n作者QQ:929842234\n点点star" 105 | var TEXT3 = "ImageSpan 静态表情:[笑脸][笑脸][瞪大眼][瞪大眼][色][色][瘪嘴][瘪嘴][酷][酷]" 106 | var TEXT4 = "ImageSpan动态表情 [好的] [羊驼]" 107 | var TEXT5 = 108 | "文本是表情 Emoji  表情:\uD83D\uDE04\uD83D\uDE03\uD83D\uDE00\uD83D\uDE0A\uD83D\uDE09\uD83D\uDE0D\uD83D\uDE18\uD83D\uDCAF\uD83D\uDD18\uD83D\uDD17\uD83D\uDD31" 109 | var TEXT6 = 110 | "富文本网络静态图和网络动态图:

" + 111 | "

\"点赞\"

" + 112 | "

\"动态图\"

" + 113 | "是不是选中了还没有背景,富文本里的ImageSpan图片也需要继承com.xiaoguang.selecttext.SelectImageSpan.kt " 114 | var TEXT7 = 115 | "\uD83D\uDD14\uD83D\uDD15✡✝\uD83D\uDD2F\uD83D\uDCDB\uD83D\uDD30\uD83D\uDD31⭕✅☑✔✖❌❎➕➖➗➰➿〽✳✴❇‼⁉❓❔❕❗©®™\uD83C\uDFA6\uD83D\uDD05\uD83D\uDD06\uD83D\uDCAF\uD83D\uDD20\uD83D\uDD21\uD83D\uDD22\uD83D\uDD23\uD83D\uDD24\uD83C\uDD70\uD83C\uDD8E\uD83C\uDD71\uD83C\uDD91\uD83C\uDD92\uD83C\uDD93ℹ\uD83C\uDD94Ⓜ\uD83C\uDD95\uD83C\uDD96\uD83C\uDD7E\uD83C\uDD97\uD83C\uDD7F\uD83C\uDD98\uD83C\uDD99\uD83C\uDD9A\uD83C\uDE01\uD83C\uDE02\uD83C\uDE37\uD83C\uDE362 现已推出,其中包含最新功能和变更,供您在应用中试用。如需开始使用,请先获取 Beta 版并更新您的工具。Android 12 Beta 版现已面向用户和开发者推出,请务必测试您的应用是否兼容,并根据需要发布任何相关更新。欢迎试用新功能,并通过我们的问题跟踪器分享反馈。" 116 | var TEXT8 = 117 | "提供更\uD83C\uDE35\uD83D\uDD34\uD83D\uDFE0\uD83D\uDFE1\uD83D\uDFE2\uD83D\uDD35\uD83D\uDFE3\uD83D\uDFE4⚫⚪\uD83D\uDFE5\uD83D\uDFE7\uD83D\uDFE8\uD83D\uDFE9\uD83D\uDFE6\uD83D\uDFEA\uD83D\uDFEB⬛⬜◼️◻️◾◽▪️▫️\uD83D\uDD36\uD83D\uDD37\uD83D\uDD38\uD83D\uDD39\uD83D\uDD3A\uD83D\uDD3B\uD83D\uDCA0\uD83D\uDD18\uD83D\uDD33\uD83D\uDD32\uD83C\uDFC1\uD83D\uDEA9\uD83C\uDF8C\uD83C\uDFF4\uD83C\uDFF3️\uD83C\uDFF3️\u200D\uD83C\uDF08\uD83C\uDFF3️\u200D⚧️\uD83C\uDFF4\u200D☠️\uD83C\uDDE6\uD83C\uDDE8\uD83C\uDDE6\uD83C\uDDE9\uD83C\uDDE6\uD83C\uDDEA\uD83C\uDDE6\uD83C\uDDEB\uD83C\uDDE6\uD83C\uDDEC\uD83C\uDDE6\uD83C\uDDEE\uD83C\uDDE6\uD83C\uDDF1\uD83C\uDDE6\uD83C\uDDF2\uD83C\uDDE6\uD83C\uDDF4\uD83C\uDDE6\uD83C\uDDF6\uD83C\uDDE6\uD83C\uDDF7\uD83C\uDDE6\uD83C\uDDF8\uD83C\uDDE6\uD83C\uDDF9\uD83C\uDDE6\uD83C\uDDFA\uD83C\uDDE6\uD83C\uDDFC\uD83C\uDDE6\uD83C\uDDFD\uD83C\uDDE6\uD83C\uDDFF\uD83C\uDDE7\uD83C\uDDE6\uD83C\uDDE7\uD83C\uDDE7\uD83C\uDDE7\uD83C\uDDE9\uD83C\uDDE7\uD83C\uDDEA\uD83C\uDDE7\uD83C\uDDEB\uD83C\uDDE7\uD83C\uDDEC\uD83C\uDDE7\uD83C\uDDED\uD83C\uDDE7\uD83C\uDDEE\uD83C\uDDE7\uD83C\uDDEF\uD83C\uDDE7\uD83C\uDDF1\uD83C\uDDE7\uD83C\uDDF2\uD83C\uDDE7\uD83C\uDDF3\uD83C\uDDE7\uD83C\uDDF4\uD83C\uDDE7\uD83C\uDDF6\uD83C\uDDE7\uD83C\uDDF7\uD83C\uDDE7\uD83C\uDDF8\uD83C\uDDE7\uD83C\uDDF9\uD83C\uDDE7\uD83C\uDDFB\uD83C\uDDE7\uD83C\uDDFC\uD83C\uDDE7\uD83C\uDDFE\uD83C\uDDE7\uD83C\uDDFF\uD83C\uDDE8\uD83C\uDDE6\uD83C\uDDE8\uD83C\uDDE8\uD83C\uDDE8\uD83C\uDDE9\uD83C\uDDE8\uD83C\uDDEB\uD83C\uDDE8\uD83C\uDDEC\uD83C\uDDE8\uD83C\uDDED\uD83C\uDDE8\uD83C\uDDEE\uD83C\uDDE8\uD83C\uDDF0\uD83C\uDDE8\uD83C\uDDF1\uD83C\uDDE8\uD83C\uDDF2\uD83C\uDDE8\uD83C\uDDF3\uD83C\uDDE8\uD83C\uDDF4\uD83C\uDDE8\uD83C\uDDF5\uD83C\uDDE8\uD83C\uDDF7\uD83C\uDDE8\uD83C\uDDFA\uD83C\uDDE8\uD83C\uDDFB\uD83C\uDDE8\uD83C\uDDFC\uD83C\uDDE8\uD83C\uDDFD\uD83C\uDDE8\uD83C\uDDFE\uD83C\uDDE8\uD83C\uDDFF\uD83C\uDDE9\uD83C\uDDEA\uD83C\uDDE9\uD83C\uDDEC\uD83C\uDDE9\uD83C\uDDEF\uD83C\uDDE9\uD83C\uDDF0\uD83C\uDDE9\uD83C\uDDF2\uD83C\uDDE9\uD83C\uDDF4\uD83C\uDDE9\uD83C\uDDFF\uD83C\uDDEA\uD83C\uDDE6\uD83C\uDDEA\uD83C\uDDE8\uD83C\uDDEA\uD83C\uDDEA\uD83C\uDDEA\uD83C\uDDEC\uD83C\uDDEA\uD83C\uDDED\uD83C\uDDEA\uD83C\uDDF7\uD83C\uDDEA\uD83C\uDDF8\uD83C\uDDEA\uD83C\uDDF9\uD83C\uDDEA\uD83C\uDDFA\uD83C\uDDEB\uD83C\uDDEE\uD83C\uDDEB\uD83C\uDDEF\uD83C\uDDEB\uD83C\uDDF0\uD83C\uDDEB\uD83C\uDDF2\uD83C\uDDEB\uD83C\uDDF4\uD83C\uDDEB\uD83C\uDDF7\uD83C\uDDEC\uD83C\uDDE6\uD83C\uDDEC\uD83C\uDDE7\uD83C\uDDEC\uD83C\uDDE9\uD83C\uDDEC\uD83C\uDDEA\uD83C\uDDEC\uD83C\uDDEB\uD83C\uDDEC\uD83C\uDDEC\uD83C\uDDEC\uD83C\uDDED\uD83C\uDDEC\uD83C\uDDEE\uD83C\uDDEC\uD83C\uDDF1\uD83C\uDDEC\uD83C\uDDF2\uD83C\uDDEC\uD83C\uDDF3\uD83C\uDDEC\uD83C\uDDF5\uD83C\uDDEC\uD83C\uDDF6\uD83C\uDDEC\uD83C\uDDF7\uD83C\uDDEC\uD83C\uDDF8\uD83C\uDDEC\uD83C\uDDF9\uD83C\uDDEC\uD83C\uDDFA\uD83C\uDDEC\uD83C\uDDFC\uD83C\uDDEC\uD83C\uDDFE\uD83C\uDDED\uD83C\uDDF0\uD83C\uDDED\uD83C\uDDF2\uD83C\uDDED\uD83C\uDDF3\uD83C\uDDED\uD83C\uDDF7\uD83C\uDDED\uD83C\uDDF9\uD83C\uDDED\uD83C\uDDFA\uD83C\uDDEE\uD83C\uDDE8\uD83C\uDDEE\uD83C\uDDE9\uD83C\uDDEE\uD83C\uDDEA\uD83C\uDDEE\uD83C\uDDF1\uD83C\uDDEE\uD83C\uDDF2\uD83C\uDDEE\uD83C\uDDF3\uD83C\uDDEE\uD83C\uDDF4\uD83C\uDDEE\uD83C\uDDF6\uD83C\uDDEE\uD83C\uDDF7\uD83C\uDDEE\uD83C\uDDF8\uD83C\uDDEE\uD83C\uDDF9\uD83C\uDDEF\uD83C\uDDEA\uD83C\uDDEF\uD83C\uDDF2\uD83C\uDDEF\uD83C\uDDF4\uD83C\uDDEF\uD83C\uDDF5\uD83C\uDDF0\uD83C\uDDEA\uD83C\uDDF0\uD83C\uDDEC\uD83C\uDDF0\uD83C\uDDED\uD83C\uDDF0\uD83C\uDDEE\uD83C\uDDF0\uD83C\uDDF2\uD83C\uDDF0\uD83C\uDDF3\uD83C\uDDF0\uD83C\uDDF5\uD83C\uDDF0\uD83C\uDDF7\uD83C\uDDF0\uD83C\uDDFC\uD83C\uDDF0\uD83C\uDDFE\uD83C\uDDF0\uD83C\uDDFF\uD83C\uDDF1\uD83C\uDDE6\uD83C\uDDF1\uD83C\uDDE7\uD83C\uDDF1\uD83C\uDDE8\uD83C\uDDF1\uD83C\uDDEE\uD83C\uDDF1\uD83C\uDDF0\uD83C\uDDF1\uD83C\uDDF7\uD83C\uDDF1\uD83C\uDDF8\uD83C\uDDF1\uD83C\uDDF9\uD83C\uDDF1\uD83C\uDDFA\uD83C\uDDF1\uD83C\uDDFB\uD83C\uDDF1\uD83C\uDDFE\uD83C\uDDF2\uD83C\uDDE6\uD83C\uDDF2\uD83C\uDDE8\uD83C\uDDF2\uD83C\uDDE9\uD83C\uDDF2\uD83C\uDDEA\uD83C\uDDF2\uD83C\uDDEB\uD83C\uDDF2\uD83C\uDDEC\uD83C\uDDF2\uD83C\uDDED\uD83C\uDDF2\uD83C\uDDF0\uD83C\uDDF2\uD83C\uDDF1\uD83C\uDDF2\uD83C\uDDF2\uD83C\uDDF2\uD83C\uDDF3\uD83C\uDDF2\uD83C\uDDF4\uD83C\uDDF2\uD83C\uDDF5\uD83C\uDDF2\uD83C\uDDF6\uD83C\uDDF2\uD83C\uDDF7\uD83C\uDDF2\uD83C\uDDF8\uD83C\uDDF2\uD83C\uDDF9\uD83C\uDDF2\uD83C\uDDFA\uD83C\uDDF2\uD83C\uDDFB\uD83C\uDDF2\uD83C\uDDFC\uD83C\uDDF2\uD83C\uDDFD\uD83C\uDDF2\uD83C\uDDFE\uD83C\uDDF2\uD83C\uDDFF\uD83C\uDDF3\uD83C\uDDE6\uD83C\uDDF3\uD83C\uDDE8\uD83C\uDDF3\uD83C\uDDEA\uD83C\uDDF3\uD83C\uDDEB\uD83C\uDDF3\uD83C\uDDEC\uD83C\uDDF3\uD83C\uDDEE\uD83C\uDDF3\uD83C\uDDF1\uD83C\uDDF3\uD83C\uDDF4\uD83C\uDDF3\uD83C\uDDF5\uD83C\uDDF3\uD83C\uDDF7\uD83C\uDDF3\uD83C\uDDFA\uD83C\uDDF3\uD83C\uDDFF\uD83C\uDDF4\uD83C\uDDF2\uD83C\uDDF5\uD83C\uDDE6\uD83C\uDDF5\uD83C\uDDEA\uD83C\uDDF5\uD83C\uDDEB\uD83C\uDDF5\uD83C\uDDEC\uD83C\uDDF5\uD83C\uDDED\uD83C\uDDF5\uD83C\uDDF0\uD83C\uDDF5\uD83C\uDDF1\uD83C\uDDF5\uD83C\uDDF2\uD83C\uDDF5\uD83C\uDDF3\uD83C\uDDF5\uD83C\uDDF7\uD83C\uDDF5\uD83C\uDDF8\uD83C\uDDF5\uD83C\uDDF9\uD83C\uDDF5\uD83C\uDDFC\uD83C\uDDF5\uD83C\uDDFE\uD83C\uDDF6\uD83C\uDDE6\uD83C\uDDF7\uD83C\uDDEA\uD83C\uDDF7\uD83C\uDDF4\uD83C\uDDF7\uD83C\uDDF8\uD83C\uDDF7\uD83C\uDDFA\uD83C\uDDF7\uD83C\uDDFC\uD83C\uDDF8\uD83C\uDDE6\uD83C\uDDF8\uD83C\uDDE7\uD83C\uDDF8\uD83C\uDDE8\uD83C\uDDF8\uD83C\uDDE9\uD83C\uDDF8\uD83C\uDDEA\uD83C\uDDF8\uD83C\uDDEC\uD83C\uDDF8\uD83C\uDDED\uD83C\uDDF8\uD83C\uDDEE\uD83C\uDDF8\uD83C\uDDEF\uD83C\uDDF8\uD83C\uDDF0\uD83C\uDDF8\uD83C\uDDF1\uD83C\uDDF8\uD83C\uDDF2\uD83C\uDDF8\uD83C\uDDF3\uD83C\uDDF8\uD83C\uDDF4\uD83C\uDDF8\uD83C\uDDF7\uD83C\uDDF8\uD83C\uDDF8\uD83C\uDDF8\uD83C\uDDF9\uD83C\uDDF8\uD83C\uDDFB\uD83C\uDDF8\uD83C\uDDFD\uD83C\uDDF8\uD83C\uDDFE\uD83C\uDDF8\uD83C\uDDFF\uD83C\uDDF9\uD83C\uDDE6\uD83C\uDDF9\uD83C\uDDE8\uD83C\uDDF9\uD83C\uDDE9\uD83C\uDDF9\uD83C\uDDEB\uD83C\uDDF9\uD83C\uDDEC\uD83C\uDDF9\uD83C\uDDED\uD83C\uDDF9\uD83C\uDDEF\uD83C\uDDF9\uD83C\uDDF0\uD83C\uDDF9\uD83C\uDDF1\uD83C\uDDF9\uD83C\uDDF2\uD83C\uDDF9\uD83C\uDDF3\uD83C\uDDF9\uD83C\uDDF4\uD83C\uDDF9\uD83C\uDDF7\uD83C\uDDF9\uD83C\uDDF9\uD83C\uDDF9\uD83C\uDDFB\uD83C\uDDF9\uD83C\uDDFC\uD83C\uDDF9\uD83C\uDDFF\uD83C\uDDFA\uD83C\uDDE6\uD83C\uDDFA\uD83C\uDDEC\uD83C\uDDFA\uD83C\uDDF2\uD83C\uDDFA\uD83C\uDDF3\uD83C\uDDFA\uD83C\uDDF8\uD83C\uDDFA\uD83C\uDDFE\uD83C\uDDFA\uD83C\uDDFF\uD83C\uDDFB\uD83C\uDDE6\uD83C\uDDFB\uD83C\uDDE8\uD83C\uDDFB\uD83C\uDDEA\uD83C\uDDFB\uD83C\uDDEC\uD83C\uDDFB\uD83C\uDDEE\uD83C\uDDFB\uD83C\uDDF3\uD83C\uDDFB\uD83C\uDDFA\uD83C\uDDFC\uD83C\uDDEB\uD83C\uDDFC\uD83C\uDDF8\uD83C\uDDFD\uD83C\uDDF0\uD83C\uDDFE\uD83C\uDDEA\uD83C\uDDFE\uD83C\uDDF9\uD83C\uDDFF\uD83C\uDDE6\uD83C\uDDFF\uD83C\uDDF2\uD83C\uDDFF\uD83C\uDDFC\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74\uDB40\uDC7F\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73\uDB40\uDC7F\uD83C\uDFF4\uDB40\uDC75\uDB40\uDC73\uDB40\uDC74\uDB40\uDC78\uDB40\uDC7F\uDB40\uDC75\uDB40\uDC73\uDB40\uDC74\uDB40\uDC78\uDB40\uDC7F \uDB40\uDC75\uDB40\uDC73\uDB40\uDC74\uDB40\uDC78\uDB40\uDC7F流畅的体验" 118 | var TEXT9 = 119 | "Android 13 为通过 Wi-Fi 管理设备与附近接入点连接的应用程序引入了 NEARBY_WIFI_DEVICES 运行时权限(NEARBY_DEVICES 权限组的一部分)。调用许多常用的 Wi-Fi API 的应用程序将需要新的权限,并使应用程序能够通过 Wi-Fi 发现和连接附近的设备,而不需要位置权限。[酷]" 120 | var IMG1: String? = null 121 | var LINK1 = "https://developer.android.google.cn/" 122 | } 123 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xiaoguang/selecttextview/MsgAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.xiaoguang.selecttextview 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.net.Uri 8 | import android.text.TextUtils 9 | import android.view.Gravity 10 | import android.view.LayoutInflater 11 | import android.view.View 12 | import android.view.ViewGroup 13 | import android.widget.* 14 | import androidx.core.content.ContextCompat 15 | import androidx.recyclerview.widget.RecyclerView 16 | import com.xiaoguang.selecttext.SelectTextHelper 17 | import com.xiaoguang.selecttext.SelectTextHelper.OnSelectListener 18 | import com.zzhoujay.richtext.RichText 19 | import org.greenrobot.eventbus.Subscribe 20 | import org.greenrobot.eventbus.ThreadMode 21 | 22 | /** 23 | * 消息 24 | * hxg 2020.9.13 qq:929842234 25 | */ 26 | class MsgAdapter(private val mContext: Context, private val mList: List?) : 27 | RecyclerView.Adapter() { 28 | 29 | companion object { 30 | private const val VIEW_TYPE_1 = 1 // 文本 31 | private const val VIEW_TYPE_2 = 2 // 图片 32 | private const val VIEW_TYPE_3 = 3 // 链接 33 | 34 | // 建议 SHOW_DELAY < RESET_DELAY 35 | // 避免从一个自定义弹窗到另一个自定义弹窗过度时出现闪动的bug 36 | // https://github.com/ITxiaoguang/SelectTextHelper/issues/5 37 | private const val SHOW_DELAY = 100L // 显示自定义弹窗延迟 38 | private const val RESET_DELAY = 130L // 重置自定义弹窗延迟 39 | } 40 | 41 | override fun getItemViewType(position: Int): Int { 42 | return mList!![position].type 43 | } 44 | 45 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 46 | val inflate: View 47 | return when (viewType) { 48 | VIEW_TYPE_2 -> { 49 | inflate = 50 | LayoutInflater.from(mContext).inflate(R.layout.item_msg_img, parent, false) 51 | ViewHolderImg(inflate) 52 | } 53 | VIEW_TYPE_3 -> { 54 | inflate = 55 | LayoutInflater.from(mContext).inflate(R.layout.item_msg_link, parent, false) 56 | ViewHolderLink(inflate) 57 | } 58 | VIEW_TYPE_1 -> { 59 | inflate = 60 | LayoutInflater.from(mContext).inflate(R.layout.item_msg_text, parent, false) 61 | ViewHolderText(inflate) 62 | } 63 | else -> { 64 | inflate = 65 | LayoutInflater.from(mContext).inflate(R.layout.item_msg_text, parent, false) 66 | ViewHolderText(inflate) 67 | } 68 | } 69 | } 70 | 71 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 72 | val msgBean = mList!![position] 73 | when (holder) { 74 | is ViewHolderText -> { 75 | holder.iv_head_left.visibility = if (msgBean.isReceive) View.VISIBLE else View.GONE 76 | holder.iv_head_right.visibility = if (msgBean.isReceive) View.GONE else View.VISIBLE 77 | if (msgBean.isReceive) { 78 | setGravity(holder.textView, Gravity.START) 79 | } else { 80 | setGravity(holder.textView, Gravity.END) 81 | } 82 | 83 | // todo 方法一:富文本 需要转行成富文本形式 84 | RichText.initCacheDir(holder.textView.context.applicationContext) // 项目里初始化一次即可 85 | RichText.from(msgBean.content) 86 | .autoFix(false) // 是否自动修复宽高,默认true 87 | .autoPlay(true) // gif自动播放 88 | .singleLoad(false) // RecyclerView里设为false 若同时启动了多个RichText,会并发解析,类似于AsyncTask的executeOnExecutor 89 | .done { // 在成功回调处理 90 | // 演示消息列表选择文本 91 | holder.selectText(msgBean) 92 | } 93 | .into(holder.textView) 94 | 95 | // todo 方法二:普通文本 96 | // holder.textView.text = msgBean.content 97 | // // 演示消息列表选择文本 98 | // holder.selectText(msgBean) 99 | 100 | } 101 | is ViewHolderImg -> { 102 | holder.iv_head_left.visibility = if (msgBean.isReceive) View.VISIBLE else View.GONE 103 | holder.iv_head_right.visibility = if (msgBean.isReceive) View.GONE else View.VISIBLE 104 | holder.iv_img.setBackgroundResource(R.mipmap.ic_launcher_round) 105 | if (msgBean.isReceive) { 106 | setGravity(holder.iv_img, Gravity.START) 107 | } else { 108 | setGravity(holder.iv_img, Gravity.END) 109 | } 110 | holder.rl_container.setOnLongClickListener { 111 | showCustomPop(holder.rl_container, msgBean) 112 | true 113 | } 114 | } 115 | is ViewHolderLink -> { 116 | holder.iv_head_left.visibility = if (msgBean.isReceive) View.VISIBLE else View.GONE 117 | holder.iv_head_right.visibility = if (msgBean.isReceive) View.GONE else View.VISIBLE 118 | holder.tv_link.text = msgBean.content 119 | if (msgBean.isReceive) { 120 | setGravity(holder.tv_link, Gravity.START) 121 | } else { 122 | setGravity(holder.tv_link, Gravity.END) 123 | } 124 | holder.rl_container.setOnLongClickListener { 125 | showCustomPop(holder.rl_container, msgBean) 126 | true 127 | } 128 | } 129 | } 130 | } 131 | 132 | // 设置FrameLayout子控件的gravity参数 133 | private fun setGravity(view: View, gravity: Int) { 134 | val params = view.layoutParams as FrameLayout.LayoutParams 135 | params.gravity = gravity 136 | } 137 | 138 | private fun showCustomPop(targetView: View, msgBean: MsgBean) { 139 | val isText = msgBean.type == VIEW_TYPE_1 // 是否文本类型 140 | val msgPop = CustomPop(mContext, targetView, isText) 141 | msgPop.addItem( 142 | R.drawable.ic_msg_copy, 143 | R.string.copy, 144 | object : CustomPop.onSeparateItemClickListener { 145 | override fun onClick() { 146 | copy(null, msgBean.content) 147 | } 148 | }) 149 | msgPop.addItem(R.drawable.ic_msg_rollback, 150 | R.string.rollback, 151 | object : CustomPop.onSeparateItemClickListener { 152 | override fun onClick() { 153 | toast(R.string.rollback) 154 | } 155 | }) 156 | msgPop.addItem(R.drawable.ic_msg_forward, 157 | R.string.forward, 158 | object : CustomPop.onSeparateItemClickListener { 159 | override fun onClick() { 160 | toast(R.string.forward) 161 | } 162 | }) 163 | msgPop.addItem(R.drawable.ic_msg_collect, 164 | R.string.collect, 165 | object : CustomPop.onSeparateItemClickListener { 166 | override fun onClick() { 167 | toast(R.string.collect) 168 | } 169 | }) 170 | msgPop.addItem(R.drawable.ic_msg_select, 171 | R.string.select, 172 | object : CustomPop.onSeparateItemClickListener { 173 | override fun onClick() { 174 | toast(R.string.select) 175 | } 176 | }) 177 | msgPop.addItem(R.drawable.ic_msg_quote, 178 | R.string.quote, 179 | object : CustomPop.onSeparateItemClickListener { 180 | override fun onClick() { 181 | toast(R.string.quote) 182 | } 183 | }) 184 | msgPop.addItem(R.drawable.ic_msg_delete, 185 | R.string.delete, 186 | object : CustomPop.onSeparateItemClickListener { 187 | override fun onClick() { 188 | toast(R.string.delete) 189 | } 190 | }) 191 | // msgPop.setItemWrapContent(); // 自适应每个item 192 | msgPop.show() 193 | } 194 | 195 | private fun toast(strId: Int) { 196 | toast(mContext.getString(strId)) 197 | } 198 | 199 | private fun toast(str: String) { 200 | Toast.makeText(mContext.applicationContext, str, Toast.LENGTH_SHORT).show() 201 | } 202 | 203 | override fun getItemCount(): Int { 204 | return mList?.size ?: 0 205 | } 206 | 207 | //////////////////////////////////////////////////////////////////////////////////// 208 | //////////////////////// 演示消息列表选择文本 start /////////////////////////////// 209 | //////////////////////////////////////////////////////////////////////////////////// 210 | 211 | internal inner class ViewHolderText(itemView: View) : RecyclerView.ViewHolder(itemView) { 212 | private val text_rl_container: RelativeLayout 213 | val iv_head_left: ImageView 214 | val iv_head_right: ImageView 215 | val textView: TextView 216 | private var mSelectableTextHelper: SelectTextHelper? = null 217 | private var textMsgBean: MsgBean? = null 218 | private var selectedText: String? = null 219 | 220 | init { 221 | text_rl_container = itemView.findViewById(R.id.rl_container) 222 | iv_head_left = itemView.findViewById(R.id.iv_head_left) 223 | iv_head_right = itemView.findViewById(R.id.iv_head_right) 224 | textView = itemView.findViewById(R.id.tv_content) 225 | } 226 | 227 | /** 228 | * 演示消息列表选择文本 229 | */ 230 | fun selectText(msgBean: MsgBean) { 231 | textMsgBean = msgBean 232 | mSelectableTextHelper = SelectTextHelper.Builder(textView) // 放你的textView到这里!! 233 | .setCursorHandleColor(ContextCompat.getColor(mContext, R.color.colorAccent)) // 游标颜色 234 | .setCursorHandleSizeInDp(22f) // 游标大小 单位dp 235 | .setSelectedColor( 236 | ContextCompat.getColor( 237 | mContext, 238 | R.color.colorAccentTransparent 239 | ) 240 | ) // 选中文本的颜色 241 | .setSelectAll(true) // 初次选中是否全选 default true 242 | .setScrollShow(true) // 滚动时是否继续显示 default true 243 | .setSelectedAllNoPop(true) // 已经全选无弹窗,设置了监听会回调 onSelectAllShowCustomPop 方法 244 | .setMagnifierShow(true) // 放大镜 default true 245 | .setSelectTextLength(2)// 首次选中文本的长度 default 2 246 | .setPopDelay(100)// 弹窗延迟时间 default 100毫秒 247 | .setPopAnimationStyle(R.style.Base_Animation_AppCompat_Dialog)// 弹窗动画 default 无动画 248 | .addItem(R.drawable.ic_msg_copy, 249 | R.string.copy, 250 | object : SelectTextHelper.Builder.onSeparateItemClickListener { 251 | override fun onClick() { 252 | copy(mSelectableTextHelper, selectedText) 253 | } 254 | }).addItem(R.drawable.ic_msg_select_all, 255 | R.string.select_all, 256 | object : SelectTextHelper.Builder.onSeparateItemClickListener { 257 | override fun onClick() { 258 | selectAll() 259 | } 260 | }).addItem(R.drawable.ic_msg_forward, 261 | R.string.forward, 262 | object : SelectTextHelper.Builder.onSeparateItemClickListener { 263 | override fun onClick() { 264 | forward() 265 | } 266 | }).setPopSpanCount(5) // 设置操作弹窗每行个数 default 5 267 | .setPopStyle( 268 | R.drawable.shape_color_4c4c4c_radius_8 /*操作弹窗背*/, 269 | R.drawable.ic_arrow /*箭头图片*/ 270 | ) // 设置操作弹窗背景色、箭头图片 271 | .build() 272 | mSelectableTextHelper!!.setSelectListener(object : OnSelectListener { 273 | /** 274 | * 点击回调 275 | */ 276 | override fun onClick(v: View?, originalContent: CharSequence?) { 277 | // 拿原始文本方式 278 | clickTextView(msgBean.content!!) // 推荐 279 | // clickTextView(originalContent!!) // 不推荐 富文本可能被修改值 导致gif动不了 280 | } 281 | 282 | /** 283 | * 长按回调 284 | */ 285 | override fun onLongClick(v: View?) { 286 | postShowCustomPop(Companion.SHOW_DELAY) 287 | } 288 | 289 | /** 290 | * 选中文本回调 291 | */ 292 | override fun onTextSelected(content: CharSequence?) { 293 | selectedText = content.toString() 294 | } 295 | 296 | /** 297 | * 弹窗关闭回调 298 | */ 299 | override fun onDismiss() {} 300 | 301 | /** 302 | * 点击TextView里的url回调 303 | * 304 | * 已被下面重写 305 | * textView.setMovementMethod(new LinkMovementMethodInterceptor()); 306 | */ 307 | override fun onClickUrl(url: String?) { 308 | toast("点击了: $url") 309 | 310 | val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) 311 | mContext.startActivity(intent) 312 | } 313 | 314 | /** 315 | * 全选显示自定义弹窗回调 316 | */ 317 | override fun onSelectAllShowCustomPop() { 318 | postShowCustomPop(Companion.SHOW_DELAY) 319 | } 320 | 321 | /** 322 | * 重置回调 323 | */ 324 | override fun onReset() { 325 | SelectTextEventBus.instance.dispatch(SelectTextEvent("dismissOperatePop")) 326 | } 327 | 328 | /** 329 | * 解除自定义弹窗回调 330 | */ 331 | override fun onDismissCustomPop() { 332 | SelectTextEventBus.instance.dispatch(SelectTextEvent("dismissOperatePop")) 333 | } 334 | 335 | /** 336 | * 是否正在滚动回调 337 | */ 338 | override fun onScrolling() { 339 | removeShowSelectView() 340 | } 341 | }) 342 | 343 | // 注册 344 | if (!SelectTextEventBus.instance.isRegistered(this)) { 345 | SelectTextEventBus.instance.register(this, SelectTextEvent::class.java) 346 | } 347 | } 348 | 349 | private var downTime: Long = 0 350 | 351 | /** 352 | * 双击进入查看内容 353 | * 354 | * @param content 内容 355 | */ 356 | private fun clickTextView(content: CharSequence) { 357 | if (System.currentTimeMillis() - downTime < 500) { 358 | downTime = 0 359 | val dialog = SelectTextDialog(mContext, content) 360 | dialog.show() 361 | } else { 362 | downTime = System.currentTimeMillis() 363 | } 364 | } 365 | 366 | /** 367 | * 延迟显示CustomPop 368 | * 防抖 369 | */ 370 | private fun postShowCustomPop(duration: Long) { 371 | textView.removeCallbacks(mShowCustomPopRunnable) 372 | textView.postDelayed(mShowCustomPopRunnable, duration) 373 | } 374 | 375 | private val mShowCustomPopRunnable = 376 | Runnable { showCustomPop(text_rl_container, textMsgBean) } 377 | 378 | /** 379 | * 延迟重置 380 | * 为了支持滑动不重置 381 | */ 382 | private fun postReset(duration: Long) { 383 | textView.removeCallbacks(mShowSelectViewRunnable) 384 | textView.postDelayed(mShowSelectViewRunnable, duration) 385 | } 386 | 387 | private fun removeShowSelectView() { 388 | textView.removeCallbacks(mShowSelectViewRunnable) 389 | } 390 | 391 | private val mShowSelectViewRunnable = Runnable { mSelectableTextHelper!!.reset() } 392 | 393 | /** 394 | * 全选 395 | */ 396 | private fun selectAll() { 397 | SelectTextEventBus.instance.dispatch(SelectTextEvent("dismissAllPop")) 398 | if (null != mSelectableTextHelper) { 399 | mSelectableTextHelper!!.selectAll() 400 | } 401 | } 402 | 403 | /** 404 | * 转发 405 | */ 406 | private fun forward() { 407 | SelectTextEventBus.instance.dispatch(SelectTextEvent("dismissAllPop")) 408 | toast("转发") 409 | } 410 | 411 | /** 412 | * 自定义弹窗 413 | * 414 | * @param targetView 目标View 415 | * @param msgBean 实体 416 | */ 417 | private fun showCustomPop(targetView: View, msgBean: MsgBean?) { 418 | val isText = msgBean!!.type == VIEW_TYPE_1 // 是否文本类型 419 | val msgPop = CustomPop(mContext, targetView, isText) 420 | msgPop.addItem(R.drawable.ic_msg_copy, 421 | R.string.copy, 422 | object : CustomPop.onSeparateItemClickListener { 423 | override fun onClick() { 424 | copy(mSelectableTextHelper, selectedText) 425 | } 426 | }) 427 | msgPop.addItem(R.drawable.ic_msg_rollback, 428 | R.string.rollback, 429 | object : CustomPop.onSeparateItemClickListener { 430 | override fun onClick() { 431 | toast(R.string.rollback) 432 | } 433 | }) 434 | msgPop.addItem(R.drawable.ic_msg_forward, 435 | R.string.forward, 436 | object : CustomPop.onSeparateItemClickListener { 437 | override fun onClick() { 438 | toast(R.string.forward) 439 | } 440 | }) 441 | msgPop.addItem(R.drawable.ic_msg_collect, 442 | R.string.collect, 443 | object : CustomPop.onSeparateItemClickListener { 444 | override fun onClick() { 445 | toast(R.string.collect) 446 | } 447 | }) 448 | msgPop.addItem(R.drawable.ic_msg_select, 449 | R.string.select, 450 | object : CustomPop.onSeparateItemClickListener { 451 | override fun onClick() { 452 | toast(R.string.select) 453 | } 454 | }) 455 | msgPop.addItem(R.drawable.ic_msg_quote, 456 | R.string.quote, 457 | object : CustomPop.onSeparateItemClickListener { 458 | override fun onClick() { 459 | toast(R.string.quote) 460 | } 461 | }) 462 | msgPop.addItem(R.drawable.ic_msg_delete, 463 | R.string.delete, 464 | object : CustomPop.onSeparateItemClickListener { 465 | override fun onClick() { 466 | toast(R.string.delete) 467 | } 468 | }) 469 | // 设置每个item自适应 470 | // msgPop.setItemWrapContent(); 471 | // 设置背景 和 箭头 472 | // msgPop.setPopStyle(R.drawable.shape_color_666666_radius_8, R.drawable.ic_arrow_666); 473 | msgPop.show() 474 | } 475 | 476 | /** 477 | * 自定义SelectTextEvent 隐藏 光标 478 | */ 479 | @Subscribe(threadMode = ThreadMode.MAIN) 480 | fun handleSelector(event: SelectTextEvent) { 481 | if (null == mSelectableTextHelper) { 482 | return 483 | } 484 | val type = event.type 485 | if (TextUtils.isEmpty(type)) { 486 | return 487 | } 488 | when (type) { 489 | "dismissAllPop" -> mSelectableTextHelper!!.reset() 490 | "dismissAllPopDelayed" -> postReset(Companion.RESET_DELAY) 491 | } 492 | } 493 | 494 | } 495 | 496 | /** 497 | * 复制 498 | */ 499 | private fun copy(mSelectableTextHelper: SelectTextHelper?, selectedText: String?) { 500 | SelectTextEventBus.instance.dispatch(SelectTextEvent("dismissAllPop")) 501 | val cm = mContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 502 | cm.setPrimaryClip(ClipData.newPlainText(selectedText, selectedText)) 503 | mSelectableTextHelper?.reset() 504 | toast("已复制") 505 | } 506 | 507 | override fun onViewRecycled(holder: RecyclerView.ViewHolder) { 508 | super.onViewRecycled(holder) 509 | if (holder is ViewHolderText) { 510 | // 注销 511 | SelectTextEventBus.instance.unregister(holder) 512 | } 513 | } 514 | 515 | //////////////////////////////////////////////////////////////////////////////////// 516 | //////////////////////// 演示消息列表选择文本 end /////////////////////////////// 517 | //////////////////////////////////////////////////////////////////////////////////// 518 | 519 | internal inner class ViewHolderImg(itemView: View) : RecyclerView.ViewHolder(itemView) { 520 | val rl_container: RelativeLayout 521 | val iv_head_left: ImageView 522 | val iv_head_right: ImageView 523 | val iv_img: ImageView 524 | 525 | init { 526 | rl_container = itemView.findViewById(R.id.rl_container) 527 | iv_head_left = itemView.findViewById(R.id.iv_head_left) 528 | iv_head_right = itemView.findViewById(R.id.iv_head_right) 529 | iv_img = itemView.findViewById(R.id.iv_img) 530 | } 531 | } 532 | 533 | internal inner class ViewHolderLink(itemView: View) : RecyclerView.ViewHolder(itemView) { 534 | val rl_container: RelativeLayout 535 | val iv_head_left: ImageView 536 | val iv_head_right: ImageView 537 | val tv_link: TextView 538 | 539 | init { 540 | rl_container = itemView.findViewById(R.id.rl_container) 541 | iv_head_left = itemView.findViewById(R.id.iv_head_left) 542 | iv_head_right = itemView.findViewById(R.id.iv_head_right) 543 | tv_link = itemView.findViewById(R.id.tv_link) 544 | } 545 | } 546 | 547 | } -------------------------------------------------------------------------------- /selecttext/src/main/java/com/xiaoguang/selecttext/SelectTextHelper.kt: -------------------------------------------------------------------------------- 1 | package com.xiaoguang.selecttext 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.graphics.Canvas 6 | import android.graphics.Color 7 | import android.graphics.Paint 8 | import android.os.Build 9 | import android.text.* 10 | import android.text.method.LinkMovementMethod 11 | import android.text.style.BackgroundColorSpan 12 | import android.text.style.ClickableSpan 13 | import android.text.style.URLSpan 14 | import android.util.Pair 15 | import android.view.* 16 | import android.view.View.OnTouchListener 17 | import android.view.ViewTreeObserver.OnScrollChangedListener 18 | import android.widget.ImageView 19 | import android.widget.Magnifier 20 | import android.widget.PopupWindow 21 | import android.widget.TextView 22 | import androidx.annotation.ColorInt 23 | import androidx.annotation.DrawableRes 24 | import androidx.annotation.StringRes 25 | import androidx.recyclerview.widget.GridLayoutManager 26 | import androidx.recyclerview.widget.RecyclerView 27 | import com.xiaoguang.selecttext.SelectTextPopAdapter.onClickItemListener 28 | import java.util.* 29 | 30 | 31 | /** 32 | * Created by hxg on 2021/9/13 929842234@qq.com 33 | * 34 | * 仿照的例子:https://github.com/laobie 35 | * 放大镜 Magnifier:https://developer.android.google.cn/guide/topics/text/magnifier 36 | */ 37 | class SelectTextHelper(builder: Builder) { 38 | private var mTextView: TextView 39 | 40 | private var mOriginalContent: CharSequence // 原本的文本 41 | private var mStartHandle: CursorHandle? = null // 开始操作标 42 | private var mEndHandle: CursorHandle? = null // 结束操作标 43 | private var mOperateWindow: OperateWindow? = null // 操作弹窗 44 | private var mMagnifier: Magnifier? = null // 放大镜组件 45 | private val mSelectionInfo = SelectionInfo() 46 | private var mSelectListener: OnSelectListener? = null 47 | private val mContext: Context 48 | private var mSpannable: Spannable? = null 49 | private var mTouchX = 0 50 | private var mTouchY = 0 51 | private var mTextViewMarginStart = 0 // textView的marginStart值 52 | private val mSelectedColor: Int // 选中文本的颜色 53 | private val mCursorHandleColor: Int // 游标的颜色 54 | private val mCursorHandleSize: Int // 游标大小 55 | private val mSelectAll: Boolean // 全选 56 | private val mSelectedAllNoPop: Boolean // 已经全选无弹窗 57 | private val mSelectTextLength: Int // 首次选择文字长度 58 | private val mScrollShow: Boolean // 滑动依然显示弹窗 59 | private val mMagnifierShow: Boolean // 显示放大镜 60 | private val mPopSpanCount: Int // 弹窗每行个数 61 | private val mPopBgResource: Int // 弹窗箭头 62 | private val mPopDelay: Int // 弹窗延迟时间 63 | private val mPopAnimationStyle: Int // 弹窗动画 64 | private val mPopArrowImg: Int // 弹窗箭头 65 | private val itemTextList: List> // 操作弹窗item文本 66 | private var itemListenerList: List = 67 | LinkedList() // 操作弹窗item监听 68 | private var mSpan: BackgroundColorSpan? = null 69 | private var isHideWhenScroll = false 70 | private var isHide = true 71 | private var usedClickListener = false // 消费了点击事件 72 | private var mOnPreDrawListener: ViewTreeObserver.OnPreDrawListener? = null 73 | private var mOnScrollChangedListener: OnScrollChangedListener? = null 74 | private var mRootTouchListener: OnTouchListener? = null 75 | 76 | init { 77 | mTextView = builder.mTextView 78 | mOriginalContent = mTextView.text 79 | mContext = mTextView.context 80 | mSelectedColor = builder.mSelectedColor 81 | mCursorHandleColor = builder.mCursorHandleColor 82 | mSelectAll = builder.mSelectAll 83 | mScrollShow = builder.mScrollShow 84 | mMagnifierShow = builder.mMagnifierShow 85 | mSelectedAllNoPop = builder.mSelectedAllNoPop 86 | mSelectTextLength = builder.mSelectTextLength 87 | mPopSpanCount = builder.mPopSpanCount 88 | mPopBgResource = builder.mPopBgResource 89 | mPopDelay = builder.mPopDelay 90 | mPopAnimationStyle = builder.mPopAnimationStyle 91 | mPopArrowImg = builder.mPopArrowImg 92 | itemTextList = builder.itemTextList 93 | itemListenerList = builder.itemListenerList 94 | mCursorHandleSize = SelectUtils.dp2px(builder.mCursorHandleSizeInDp) 95 | init() 96 | } 97 | 98 | /** 99 | * public start 100 | */ 101 | companion object { 102 | private const val DEFAULT_SELECTION_LENGTH = 2 // 选2个字节长度 例:表情属于2个字节 103 | private const val DEFAULT_SHOW_DURATION = 100 // 弹窗100毫秒延迟 104 | 105 | @Volatile 106 | var emojiMap: MutableMap = HashMap() 107 | 108 | @Synchronized 109 | fun putAllEmojiMap(map: Map?) { 110 | emojiMap.putAll(map!!) 111 | } 112 | 113 | @Synchronized 114 | fun putEmojiMap(emojiKey: String, @DrawableRes drawableRes: Int) { 115 | emojiMap[emojiKey] = drawableRes 116 | } 117 | 118 | } 119 | 120 | open class OnSelectListenerImpl : OnSelectListener { 121 | override fun onClick(v: View?, originalContent: CharSequence?) = Unit 122 | override fun onLongClick(v: View?) = Unit 123 | override fun onTextSelected(content: CharSequence?) = Unit 124 | override fun onDismiss() = Unit 125 | override fun onClickUrl(url: String?) = Unit 126 | override fun onSelectAllShowCustomPop() = Unit 127 | override fun onReset() = Unit 128 | override fun onDismissCustomPop() = Unit 129 | override fun onScrolling() = Unit 130 | } 131 | 132 | interface OnSelectListener { 133 | fun onClick(v: View?, originalContent: CharSequence?) // 点击textView 134 | fun onLongClick(v: View?) // 长按textView 135 | fun onTextSelected(content: CharSequence?) // 选中文本回调 136 | fun onDismiss() // 解除弹窗回调 137 | fun onClickUrl(url: String?) // 点击文本里的url回调 138 | fun onSelectAllShowCustomPop() // 全选显示自定义弹窗回调 139 | fun onReset() // 重置回调 140 | fun onDismissCustomPop() // 解除自定义弹窗回调 141 | fun onScrolling() // 正在滚动回调 142 | } 143 | 144 | class Builder(val mTextView: TextView) { 145 | var mCursorHandleColor = -0xec862a 146 | var mSelectedColor = -0x501e0c 147 | var mCursorHandleSizeInDp = 24f 148 | var mSelectAll = true 149 | var mSelectedAllNoPop = false 150 | var mSelectTextLength = DEFAULT_SELECTION_LENGTH 151 | var mScrollShow = true 152 | var mMagnifierShow = true 153 | var mPopSpanCount = 5 154 | var mPopBgResource = 0 155 | var mPopDelay = DEFAULT_SHOW_DURATION 156 | var mPopAnimationStyle = 0 157 | var mPopArrowImg = 0 158 | val itemTextList: MutableList> = LinkedList() 159 | val itemListenerList: MutableList = LinkedList() 160 | 161 | /** 162 | * 选择游标颜色 163 | */ 164 | fun setCursorHandleColor(@ColorInt cursorHandleColor: Int): Builder { 165 | mCursorHandleColor = cursorHandleColor 166 | return this 167 | } 168 | 169 | /** 170 | * 选择游标大小 171 | */ 172 | fun setCursorHandleSizeInDp(cursorHandleSizeInDp: Float): Builder { 173 | mCursorHandleSizeInDp = cursorHandleSizeInDp 174 | return this 175 | } 176 | 177 | /** 178 | * 选中文本的颜色 179 | */ 180 | fun setSelectedColor(@ColorInt selectedBgColor: Int): Builder { 181 | mSelectedColor = selectedBgColor 182 | return this 183 | } 184 | 185 | /** 186 | * 全选 187 | */ 188 | fun setSelectAll(selectAll: Boolean): Builder { 189 | mSelectAll = selectAll 190 | return this 191 | } 192 | 193 | /** 194 | * 已经全选无弹窗 195 | */ 196 | fun setSelectedAllNoPop(selectedAllNoPop: Boolean): Builder { 197 | mSelectedAllNoPop = selectedAllNoPop 198 | return this 199 | } 200 | 201 | /** 202 | * 选择选择个数 203 | */ 204 | fun setSelectTextLength(selectTextLength: Int): Builder { 205 | mSelectTextLength = selectTextLength 206 | return this 207 | } 208 | 209 | /** 210 | * 滑动依然显示弹窗 211 | */ 212 | fun setScrollShow(scrollShow: Boolean): Builder { 213 | mScrollShow = scrollShow 214 | return this 215 | } 216 | 217 | /** 218 | * 显示放大镜 219 | */ 220 | fun setMagnifierShow(magnifierShow: Boolean): Builder { 221 | mMagnifierShow = magnifierShow 222 | return this 223 | } 224 | 225 | /** 226 | * 弹窗每行个数 227 | */ 228 | fun setPopSpanCount(popSpanCount: Int): Builder { 229 | mPopSpanCount = popSpanCount 230 | return this 231 | } 232 | 233 | /** 234 | * 弹窗背景颜色、弹窗箭头 235 | */ 236 | fun setPopStyle(popBgResource: Int, popArrowImg: Int): Builder { 237 | mPopBgResource = popBgResource 238 | mPopArrowImg = popArrowImg 239 | return this 240 | } 241 | 242 | /** 243 | * 弹窗延迟 244 | */ 245 | fun setPopDelay(popDelay: Int): Builder { 246 | mPopDelay = popDelay 247 | return this 248 | } 249 | 250 | /** 251 | * 弹窗动画 252 | */ 253 | fun setPopAnimationStyle(popAnimationStyle: Int): Builder { 254 | mPopAnimationStyle = popAnimationStyle 255 | return this 256 | } 257 | 258 | fun addItem( 259 | @DrawableRes drawableId: Int, 260 | @StringRes textResId: Int, 261 | listener: onSeparateItemClickListener 262 | ): Builder { 263 | itemTextList.add(Pair(drawableId, mTextView.context.resources.getString(textResId))) 264 | itemListenerList.add(listener) 265 | return this 266 | } 267 | 268 | fun addItem( 269 | @DrawableRes drawableId: Int, itemText: String, listener: onSeparateItemClickListener 270 | ): Builder { 271 | itemTextList.add(Pair(drawableId, itemText)) 272 | itemListenerList.add(listener) 273 | return this 274 | } 275 | 276 | fun addItem(@StringRes textResId: Int, listener: onSeparateItemClickListener): Builder { 277 | itemTextList.add(Pair(0, mTextView.context.resources.getString(textResId))) 278 | itemListenerList.add(listener) 279 | return this 280 | } 281 | 282 | fun addItem(itemText: String, listener: onSeparateItemClickListener): Builder { 283 | itemTextList.add(Pair(0, itemText)) 284 | itemListenerList.add(listener) 285 | return this 286 | } 287 | 288 | fun build(): SelectTextHelper { 289 | return SelectTextHelper(this) 290 | } 291 | 292 | interface onSeparateItemClickListener { 293 | fun onClick() 294 | } 295 | } 296 | 297 | /** 298 | * 重置弹窗 299 | */ 300 | fun reset() { 301 | hideSelectView() 302 | resetSelectionInfo() 303 | // 重置弹窗回调 304 | mSelectListener?.onReset() 305 | } 306 | 307 | /** 308 | * 操作弹窗是否显示中 309 | */ 310 | val isPopShowing: Boolean 311 | get() = if (null != mOperateWindow) { 312 | mOperateWindow!!.isShowing 313 | } else false 314 | 315 | /** 316 | * 选择文本监听 317 | */ 318 | fun setSelectListener(selectListener: OnSelectListener?) { 319 | mSelectListener = selectListener 320 | } 321 | 322 | /** 323 | * 销毁 324 | */ 325 | fun destroy() { 326 | mTextView.viewTreeObserver.removeOnScrollChangedListener(mOnScrollChangedListener) 327 | mTextView.viewTreeObserver.removeOnPreDrawListener(mOnPreDrawListener) 328 | mTextView.rootView.setOnTouchListener(null) 329 | reset() 330 | mStartHandle = null 331 | mEndHandle = null 332 | mOperateWindow = null 333 | } 334 | 335 | /** 336 | * 全选 337 | */ 338 | fun selectAll() { 339 | hideSelectView() 340 | selectText(0, mTextView.text.length) 341 | isHide = false 342 | showCursorHandle(mStartHandle) 343 | showCursorHandle(mEndHandle) 344 | showOperateWindow() 345 | } 346 | 347 | /** 348 | * public end 349 | */ 350 | 351 | @SuppressLint("ClickableViewAccessibility") 352 | private fun init() { 353 | val spanStr = SpannableStringBuilder(mOriginalContent) 354 | // 处理空格 把不间断空格(\u00A0)/半角空格(\u0020)转成全角空格(\u3000) 355 | // 为什么处理2个,而不是1个呢? 356 | // 避免英文单词出现断节 357 | SelectUtils.replaceContent(spanStr, mOriginalContent, "\u00A0\u00A0", "\u3000\u3000") 358 | SelectUtils.replaceContent(spanStr, mOriginalContent, "\u0020\u0020", "\u3000\u3000") 359 | // 文字转化成图片背景 360 | SelectUtils.replaceText2Emoji(mContext, emojiMap, spanStr, mOriginalContent) 361 | 362 | // 去除超链接点击背景色 https://github.com/ITxiaoguang/SelectTextHelper/issues/2 363 | mTextView.highlightColor = Color.TRANSPARENT 364 | mTextView.setText(spanStr, TextView.BufferType.SPANNABLE) 365 | mTextView.setOnTouchListener { _: View?, event: MotionEvent -> 366 | mTouchX = event.x.toInt() 367 | mTouchY = event.y.toInt() 368 | false 369 | } 370 | mTextView.setOnClickListener { 371 | if (usedClickListener) { 372 | usedClickListener = false 373 | return@setOnClickListener 374 | } 375 | if (null == mOperateWindow || !mOperateWindow!!.isShowing) { 376 | mSelectListener?.onDismiss() 377 | } 378 | reset() 379 | mSelectListener?.onClick(mTextView, mOriginalContent) 380 | } 381 | mTextView.setOnLongClickListener { 382 | mTextView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { 383 | override fun onViewAttachedToWindow(v: View) {} 384 | override fun onViewDetachedFromWindow(v: View) { 385 | destroy() 386 | } 387 | }) 388 | mOnPreDrawListener = ViewTreeObserver.OnPreDrawListener { 389 | if (isHideWhenScroll) { 390 | isHideWhenScroll = false 391 | postShowSelectView(mPopDelay) 392 | } 393 | // 拿textView的x坐标 394 | if (0 == mTextViewMarginStart) { 395 | val location = IntArray(2) 396 | mTextView.getLocationInWindow(location) 397 | mTextViewMarginStart = location[0] 398 | } 399 | return@OnPreDrawListener true 400 | } 401 | mTextView.viewTreeObserver.addOnPreDrawListener(mOnPreDrawListener) 402 | 403 | // 根布局监听 404 | mRootTouchListener = OnTouchListener { _, _ -> 405 | reset() 406 | mTextView.rootView.setOnTouchListener(null) 407 | return@OnTouchListener false 408 | } 409 | mTextView.rootView.setOnTouchListener(mRootTouchListener) 410 | mOnScrollChangedListener = OnScrollChangedListener { 411 | if (mScrollShow) { 412 | if (!isHideWhenScroll && !isHide) { 413 | isHideWhenScroll = true 414 | mOperateWindow?.dismiss() 415 | mStartHandle?.dismiss() 416 | mEndHandle?.dismiss() 417 | } 418 | mSelectListener?.onScrolling() 419 | } else { 420 | reset() 421 | } 422 | } 423 | mTextView.viewTreeObserver.addOnScrollChangedListener(mOnScrollChangedListener) 424 | if (null == mOperateWindow) { 425 | mOperateWindow = OperateWindow(mContext) 426 | } 427 | if (mSelectAll) { 428 | showAllView() 429 | } else { 430 | showSelectView(mTouchX, mTouchY) 431 | } 432 | mSelectListener?.onLongClick(mTextView) 433 | true 434 | } 435 | // 此setMovementMethod可被修改 436 | mTextView.movementMethod = LinkMovementMethodInterceptor() 437 | } 438 | 439 | private fun postShowSelectView(duration: Int) { 440 | mTextView.removeCallbacks(mShowSelectViewRunnable) 441 | if (duration <= 0) { 442 | mShowSelectViewRunnable.run() 443 | } else { 444 | mTextView.postDelayed(mShowSelectViewRunnable, duration.toLong()) 445 | } 446 | } 447 | 448 | private val mShowSelectViewRunnable = Runnable { 449 | if (isHide) return@Runnable 450 | if (null != mOperateWindow) { 451 | showOperateWindow() 452 | } 453 | mStartHandle?.let { showCursorHandle(mStartHandle) } 454 | mEndHandle?.let { showCursorHandle(mEndHandle) } 455 | } 456 | 457 | private fun hideSelectView() { 458 | isHide = true 459 | usedClickListener = false 460 | mStartHandle?.dismiss() 461 | mEndHandle?.dismiss() 462 | mOperateWindow?.dismiss() 463 | } 464 | 465 | private fun resetSelectionInfo() { 466 | resetEmojiBackground() 467 | mSelectionInfo.mSelectionContent = null 468 | if (mSpannable != null && mSpan != null) { 469 | mSpannable!!.removeSpan(mSpan) 470 | mSpan = null 471 | } 472 | } 473 | 474 | /** 475 | * @param x 长按时的手指的x坐标 476 | * @param y 长按时的手指的y坐标 477 | */ 478 | private fun showSelectView(x: Int, y: Int) { 479 | reset() 480 | isHide = false 481 | if (mStartHandle == null) mStartHandle = CursorHandle(true) 482 | if (mEndHandle == null) mEndHandle = CursorHandle(false) 483 | val startOffset = SelectUtils.getPreciseOffset(mTextView, x, y) 484 | var endOffset = startOffset + mSelectTextLength 485 | if (mTextView.text is Spannable) { 486 | mSpannable = mTextView.text as Spannable 487 | } 488 | if (mSpannable == null || endOffset - 1 >= mTextView.text.length) { 489 | endOffset = mTextView.text.length 490 | } 491 | endOffset = changeEndOffset(startOffset, endOffset) 492 | selectText(startOffset, endOffset) 493 | showCursorHandle(mStartHandle) 494 | showCursorHandle(mEndHandle) 495 | showOperateWindow() 496 | } 497 | 498 | /** 499 | * 处理endOffset位置 500 | * ImageSpan文本,则会加够ImageSpan匹配的字符 501 | * Emoji文本,则去除最后的文字emoji字符 502 | * 503 | * @param startOffset 开始文字坐标 504 | * @param endOffset 结束文字坐标 505 | * @return endOffset 506 | */ 507 | private fun changeEndOffset(startOffset: Int, endOffset: Int): Int { 508 | var endOffsetCopy = endOffset 509 | var selectText = 510 | mSpannable!!.subSequence(startOffset, endOffsetCopy) as Spannable 511 | // 是否ImageSpan文本 512 | if (SelectUtils.isImageSpanText(selectText)) { 513 | // 是否匹配Image 514 | while (!SelectUtils.matchImageSpan(emojiMap, selectText.toString())) { 515 | endOffsetCopy++ 516 | selectText = mSpannable!!.subSequence(startOffset, endOffsetCopy) as Spannable 517 | } 518 | } 519 | // 选中的文字倒数第二个是文字 且 倒数第一个字符是文字emoji 520 | // 则去除最后的文字emoji字符 521 | val selectTextString = selectText.toString() 522 | if (selectTextString.length > 1) { 523 | if (!SelectUtils.isEmojiText(selectTextString[selectTextString.length - 2]) 524 | && SelectUtils.isEmojiText(selectTextString[selectTextString.length - 1]) 525 | ) { 526 | endOffsetCopy-- 527 | } 528 | } 529 | return endOffsetCopy 530 | } 531 | 532 | /** 533 | * 显示操作弹窗 534 | * 可能多次调用 535 | */ 536 | private fun showOperateWindow() { 537 | if (null == mOperateWindow) { 538 | mOperateWindow = OperateWindow(mContext) 539 | } // 开启已经全选无弹窗 540 | if (mSelectedAllNoPop && mSelectionInfo.mSelectionContent.toString() == mTextView.text.toString()) { 541 | mOperateWindow!!.dismiss() 542 | mSelectListener?.onSelectAllShowCustomPop() 543 | } else { 544 | mOperateWindow!!.show() 545 | } 546 | } 547 | 548 | /** 549 | * 全选 550 | * Select all 551 | */ 552 | private fun showAllView() { 553 | reset() 554 | isHide = false 555 | if (mStartHandle == null) mStartHandle = CursorHandle(true) 556 | if (mEndHandle == null) mEndHandle = CursorHandle(false) 557 | if (mTextView.text is Spannable) { 558 | mSpannable = mTextView.text as Spannable 559 | } 560 | if (mSpannable == null) { 561 | return 562 | } 563 | selectText(0, mTextView.text.length) 564 | showCursorHandle(mStartHandle) 565 | showCursorHandle(mEndHandle) 566 | showOperateWindow() 567 | } 568 | 569 | private fun showCursorHandle(cursorHandle: CursorHandle?) { 570 | val layout = mTextView.layout 571 | val offset = if (cursorHandle!!.isLeft) mSelectionInfo.mStart else mSelectionInfo.mEnd 572 | var x = layout.getPrimaryHorizontal(offset).toInt() 573 | var y = layout.getLineBottom(layout.getLineForOffset(offset)) 574 | 575 | // 右游标 576 | // mSelectionInfo.mEnd != 0 不是第一位 577 | // x == 0 右游标在最后面 578 | // 把右游标水平坐标定位在减去一个字的线条右侧 579 | // 把右游标底部线坐标定位在上一行 580 | if (!cursorHandle.isLeft && mSelectionInfo.mEnd != 0 && x == 0) { 581 | x = layout.getLineRight(layout.getLineForOffset(mSelectionInfo.mEnd - 1)).toInt() 582 | y = layout.getLineBottom(layout.getLineForOffset(mSelectionInfo.mEnd - 1)) 583 | } 584 | cursorHandle.show(x, y) 585 | } 586 | 587 | private fun selectText(startPos: Int, endPos: Int) { 588 | if (startPos != -1) { 589 | mSelectionInfo.mStart = startPos 590 | } 591 | if (endPos != -1) { 592 | mSelectionInfo.mEnd = endPos 593 | } 594 | if (mSelectionInfo.mStart > mSelectionInfo.mEnd) { 595 | val temp = mSelectionInfo.mStart 596 | mSelectionInfo.mStart = mSelectionInfo.mEnd 597 | mSelectionInfo.mEnd = temp 598 | } 599 | if (mSpannable != null) { 600 | if (mSpan == null) { 601 | mSpan = BackgroundColorSpan(mSelectedColor) 602 | } 603 | mSelectionInfo.mSelectionContent = 604 | mSpannable!!.subSequence(mSelectionInfo.mStart, mSelectionInfo.mEnd).toString() 605 | mSpannable!!.setSpan( 606 | mSpan, mSelectionInfo.mStart, mSelectionInfo.mEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE 607 | ) 608 | mSelectListener?.onTextSelected(mSelectionInfo.mSelectionContent) 609 | 610 | // 设置图片表情选中背景 611 | setEmojiBackground() 612 | } 613 | } 614 | 615 | /** 616 | * 设置图片表情选中背景 617 | */ 618 | private fun setEmojiBackground() { 619 | if (emojiMap.isEmpty()) { 620 | return 621 | } 622 | 623 | // 前部 透明背景 624 | setEmojiBackground( 625 | mSpannable!!.subSequence(0, mSelectionInfo.mStart) as Spannable, 626 | Color.TRANSPARENT 627 | ) 628 | // 中间 选择背景 629 | setEmojiBackground( 630 | mSpannable!!.subSequence( 631 | mSelectionInfo.mStart, 632 | mSelectionInfo.mEnd 633 | ) as Spannable, mSelectedColor 634 | ) 635 | // 尾部 透明背景 636 | setEmojiBackground( 637 | mSpannable!!.subSequence( 638 | mSelectionInfo.mEnd, 639 | mSpannable!!.length 640 | ) as Spannable, Color.TRANSPARENT 641 | ) 642 | } 643 | 644 | /** 645 | * 利用反射改变图片背景颜色 646 | * 647 | * @param mSpannable Spannable 648 | * @param bgColor background 649 | */ 650 | private fun setEmojiBackground(mSpannable: Spannable, @ColorInt bgColor: Int) { 651 | if (TextUtils.isEmpty(mSpannable)) { 652 | return 653 | } 654 | val mSpans = SelectUtils.getFieldValue(mSpannable, "mSpans") as Array<*>? 655 | if (null != mSpans) { 656 | for (mSpan in mSpans) { 657 | if (mSpan is SelectImageSpan) { 658 | if (mSpan.bgColor != bgColor) { 659 | mSpan.bgColor = bgColor 660 | } 661 | } 662 | } 663 | } 664 | } 665 | 666 | /** 667 | * 重置emoji选择背景 668 | */ 669 | private fun resetEmojiBackground() { 670 | mSpannable?.let { setEmojiBackground(it, Color.TRANSPARENT) } 671 | } 672 | 673 | /** 674 | * 操作弹窗 675 | * 提供全选时可另外配置自定义弹窗 676 | * 自定义功能:复制、全选、等等 677 | * Custom function:Copy, Select all, And so on. 678 | */ 679 | @SuppressLint("InflateParams") 680 | private inner class OperateWindow(context: Context?) { 681 | private val mWindow: PopupWindow? 682 | private val mTempCoors = IntArray(2) 683 | private val mWidth: Int 684 | private val mHeight: Int 685 | private val listAdapter: SelectTextPopAdapter 686 | private val rvContent: RecyclerView? 687 | private val ivArrow: ImageView 688 | 689 | init { 690 | val contentView = LayoutInflater.from(context).inflate(R.layout.pop_operate, null) 691 | rvContent = contentView.findViewById(R.id.rv_content) 692 | ivArrow = contentView.findViewById(R.id.iv_arrow) 693 | if (0 != mPopBgResource) { 694 | rvContent.setBackgroundResource(mPopBgResource) 695 | } 696 | if (0 != mPopArrowImg) { 697 | ivArrow.setBackgroundResource(mPopArrowImg) 698 | } 699 | val size = itemTextList.size 700 | // 宽 个数超过mPopSpanCount 取 mPopSpanCount 701 | mWidth = SelectUtils.dp2px((12 * 4 + 52 * size.coerceAtMost(mPopSpanCount)).toFloat()) 702 | // 行数 703 | val row = (size / mPopSpanCount // 行数 704 | + if (size % mPopSpanCount == 0) 0 else 1) // 有余数 加一行 705 | // 高 706 | mHeight = SelectUtils.dp2px((12 * (1 + row) + 52 * row + 5).toFloat()) 707 | mWindow = PopupWindow( 708 | contentView, 709 | ViewGroup.LayoutParams.WRAP_CONTENT, 710 | ViewGroup.LayoutParams.WRAP_CONTENT, 711 | false 712 | ) 713 | mWindow.isClippingEnabled = false 714 | // 动画 715 | if (0 != mPopAnimationStyle) { 716 | mWindow.animationStyle = mPopAnimationStyle 717 | } 718 | listAdapter = SelectTextPopAdapter(context!!, itemTextList) 719 | listAdapter.setOnclickItemListener(object : onClickItemListener { 720 | override fun onClick(position: Int) { 721 | dismiss() 722 | itemListenerList[position].onClick() 723 | } 724 | }) 725 | rvContent.adapter = listAdapter 726 | } 727 | 728 | fun show() { 729 | val deviceWidth = SelectUtils.displayWidth 730 | val size = itemTextList.size 731 | if (size > mPopSpanCount) { 732 | rvContent!!.layoutManager = 733 | GridLayoutManager(mContext, mPopSpanCount, GridLayoutManager.VERTICAL, false) 734 | } else { 735 | rvContent!!.layoutManager = 736 | GridLayoutManager(mContext, size, GridLayoutManager.VERTICAL, false) 737 | } 738 | mTextView.getLocationInWindow(mTempCoors) 739 | val layout = mTextView.layout 740 | var posX: Int 741 | var posXTemp = 0 742 | val startX = layout.getPrimaryHorizontal(mSelectionInfo.mStart).toInt() + mTempCoors[0] 743 | val startY = layout.getLineTop(layout.getLineForOffset(mSelectionInfo.mStart)) 744 | val endY = layout.getLineTop(layout.getLineForOffset(mSelectionInfo.mEnd)) 745 | var posY = startY + mTempCoors[1] - mHeight 746 | if (posY < 0) posY = 0 747 | 748 | // 在同一行 749 | posX = if (startY == endY) { 750 | val endX = layout.getPrimaryHorizontal(mSelectionInfo.mEnd) 751 | .toInt() + mTempCoors[0] // posX = (起始点 + 终点) / 2 - (向左移动 mWidth / 2) 752 | (startX + endX) / 2 - mWidth / 2 753 | } else { 754 | // posX = (起始点 + (文本左边距 + 文本宽度 - 文本右padding)) / 2 - (向左移动 mWidth / 2) 755 | (startX + (mTempCoors[0] + mTextView.width - mTextView.paddingRight)) / 2 - mWidth / 2 756 | } 757 | if (posX <= 0) { 758 | posXTemp = posX 759 | posX = 0 760 | } else if (posX + mWidth > deviceWidth) { 761 | posXTemp = posX 762 | posX = deviceWidth - mWidth 763 | } 764 | mWindow!!.showAtLocation(mTextView, Gravity.NO_GRAVITY, posX, posY) // view中心位置 765 | // 在中间 766 | var arrowTranslationX = when { 767 | posXTemp == 0 -> { 768 | // - SelectUtils.dp2px(mContext, 16) 是 margin 769 | mWidth / 2 - SelectUtils.dp2px(16f) 770 | } 771 | posXTemp < 0 -> { 772 | posXTemp + mWidth / 2 773 | } 774 | else -> { 775 | // arrowTranslationX = 两坐标中心点 - 弹窗左侧点 - iv_arrow的margin 776 | posXTemp + mWidth / 2 - posX - SelectUtils.dp2px(16f) 777 | } 778 | } 779 | if (arrowTranslationX < SelectUtils.dp2px(4f)) { 780 | arrowTranslationX = SelectUtils.dp2px(4f) 781 | } else if (arrowTranslationX > mWidth - SelectUtils.dp2px(4f)) { 782 | arrowTranslationX = mWidth - SelectUtils.dp2px(4f) 783 | } 784 | ivArrow.translationX = arrowTranslationX.toFloat() 785 | } 786 | 787 | fun dismiss() { 788 | mWindow!!.dismiss() 789 | mSelectListener?.onDismissCustomPop() 790 | } 791 | 792 | val isShowing: Boolean 793 | get() = mWindow?.isShowing ?: false 794 | } 795 | 796 | /** 797 | * 游标 798 | */ 799 | private inner class CursorHandle(var isLeft: Boolean) : View(mContext) { 800 | private var mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) 801 | private val mPopupWindow: PopupWindow 802 | private val mCircleRadius = mCursorHandleSize / 2 803 | private val mWidth = mCursorHandleSize 804 | private val mHeight = mCursorHandleSize 805 | private val mPadding = 32 // 游标padding 806 | 807 | init { 808 | mPaint.color = mCursorHandleColor 809 | mPopupWindow = PopupWindow(this) 810 | mPopupWindow.isClippingEnabled = false 811 | mPopupWindow.width = mWidth + mPadding * 2 812 | mPopupWindow.height = mHeight + mPadding / 2 813 | invalidate() 814 | } 815 | 816 | override fun onDraw(canvas: Canvas) { 817 | canvas.drawCircle( 818 | (mCircleRadius + mPadding).toFloat(), 819 | mCircleRadius.toFloat(), 820 | mCircleRadius.toFloat(), 821 | mPaint 822 | ) 823 | if (isLeft) { 824 | canvas.drawRect( 825 | (mCircleRadius + mPadding).toFloat(), 826 | 0f, 827 | (mCircleRadius * 2 + mPadding).toFloat(), 828 | mCircleRadius.toFloat(), 829 | mPaint 830 | ) 831 | } else { 832 | canvas.drawRect( 833 | mPadding.toFloat(), 834 | 0f, 835 | (mCircleRadius + mPadding).toFloat(), 836 | mCircleRadius.toFloat(), 837 | mPaint 838 | ) 839 | } 840 | } 841 | 842 | private var mAdjustX = 0 843 | private var mAdjustY = 0 844 | private var mBeforeDragStart = 0 845 | private var mBeforeDragEnd = 0 846 | 847 | @SuppressLint("ClickableViewAccessibility") 848 | override fun onTouchEvent(event: MotionEvent): Boolean { 849 | when (event.action) { 850 | MotionEvent.ACTION_DOWN -> { 851 | mBeforeDragStart = mSelectionInfo.mStart 852 | mBeforeDragEnd = mSelectionInfo.mEnd 853 | mAdjustX = event.x.toInt() 854 | mAdjustY = event.y.toInt() 855 | } 856 | MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { 857 | showOperateWindow() 858 | if (mMagnifierShow) { 859 | // android 9 放大镜 860 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && null != mMagnifier) { 861 | mMagnifier!!.dismiss() 862 | } 863 | } 864 | } 865 | MotionEvent.ACTION_MOVE -> { 866 | mOperateWindow!!.dismiss() 867 | mSelectListener?.onDismissCustomPop() 868 | val rawX = event.rawX.toInt() 869 | val rawY = event.rawY.toInt() 870 | // x y不准 x 减去textView距离x轴距离值 y减去字体大小的像素值 871 | update( 872 | rawX + mAdjustX - mWidth - mTextViewMarginStart, 873 | rawY + mAdjustY - mHeight - mTextView.textSize.toInt() 874 | ) 875 | if (mMagnifierShow) { 876 | // android 9 放大镜功能 877 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 878 | if (null == mMagnifier) { 879 | mMagnifier = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 880 | Magnifier.Builder(mTextView).build() 881 | } else { 882 | Magnifier(mTextView) 883 | } 884 | } 885 | val viewPosition = IntArray(2) 886 | mTextView.getLocationOnScreen(viewPosition) 887 | val magnifierX = rawX - viewPosition[0] 888 | val magnifierY = rawY - viewPosition[1] - SelectUtils.dp2px(32f) 889 | mMagnifier!!.show( 890 | magnifierX.toFloat(), magnifierY.coerceAtLeast(0).toFloat() 891 | ) 892 | } 893 | } 894 | } 895 | } 896 | return true 897 | } 898 | 899 | private fun changeDirection() { 900 | isLeft = !isLeft 901 | invalidate() 902 | } 903 | 904 | fun dismiss() { 905 | mPopupWindow.dismiss() 906 | } 907 | 908 | private val mTempCoors = IntArray(2) 909 | fun update(x: Int, y: Int) { 910 | var yCopy = y 911 | mTextView.getLocationInWindow(mTempCoors) 912 | val oldOffset = if (isLeft) { 913 | mSelectionInfo.mStart 914 | } else { 915 | mSelectionInfo.mEnd 916 | } 917 | yCopy -= mTempCoors[1] 918 | val offset = SelectUtils.getHysteresisOffset(mTextView, x, yCopy, oldOffset) 919 | if (offset != oldOffset) { 920 | resetSelectionInfo() 921 | if (isLeft) { 922 | if (offset > mBeforeDragEnd) { 923 | val handle = getCursorHandle(false) 924 | changeDirection() 925 | handle!!.changeDirection() 926 | mBeforeDragStart = mBeforeDragEnd 927 | selectText(mBeforeDragEnd, offset) 928 | handle.updateCursorHandle() 929 | } else { 930 | selectText(offset, -1) 931 | } 932 | updateCursorHandle() 933 | } else { 934 | if (offset < mBeforeDragStart) { 935 | val handle = getCursorHandle(true) 936 | handle!!.changeDirection() 937 | changeDirection() 938 | mBeforeDragEnd = mBeforeDragStart 939 | selectText(offset, mBeforeDragStart) 940 | handle.updateCursorHandle() 941 | } else { 942 | selectText(mBeforeDragStart, offset) 943 | } 944 | updateCursorHandle() 945 | } 946 | } 947 | } 948 | 949 | private fun updateCursorHandle() { 950 | mTextView.getLocationInWindow(mTempCoors) 951 | val layout = mTextView.layout 952 | if (isLeft) { 953 | mPopupWindow.update( 954 | layout.getPrimaryHorizontal(mSelectionInfo.mStart).toInt() - mWidth + extraX, 955 | layout.getLineBottom(layout.getLineForOffset(mSelectionInfo.mStart)) + extraY, 956 | -1, 957 | -1 958 | ) 959 | } else { 960 | var horizontalEnd = layout.getPrimaryHorizontal(mSelectionInfo.mEnd).toInt() 961 | var lineBottomEnd = 962 | layout.getLineBottom(layout.getLineForOffset(mSelectionInfo.mEnd)) // 右游标 // mSelectionInfo.mEnd != 0 不是第一位 963 | // horizontalEnd == 0 右游标在最后面 964 | // 把右游标水平坐标定位在减去一个字的线条右侧 965 | // 把右游标底部线坐标定位在上一行 966 | if (mSelectionInfo.mEnd != 0 && horizontalEnd == 0) { 967 | horizontalEnd = 968 | layout.getLineRight(layout.getLineForOffset(mSelectionInfo.mEnd - 1)) 969 | .toInt() 970 | lineBottomEnd = 971 | layout.getLineBottom(layout.getLineForOffset(mSelectionInfo.mEnd - 1)) 972 | } 973 | mPopupWindow.update(horizontalEnd + extraX, lineBottomEnd + extraY, -1, -1) 974 | } 975 | } 976 | 977 | fun show(x: Int, y: Int) { 978 | mTextView.getLocationInWindow(mTempCoors) 979 | val offset = if (isLeft) mWidth else 0 980 | mPopupWindow.showAtLocation( 981 | mTextView, Gravity.NO_GRAVITY, x - offset + extraX, y + extraY 982 | ) 983 | } 984 | 985 | val extraX: Int 986 | get() = mTempCoors[0] - mPadding + mTextView.paddingLeft 987 | val extraY: Int 988 | get() = mTempCoors[1] + mTextView.paddingTop 989 | } 990 | 991 | private fun getCursorHandle(isLeft: Boolean): CursorHandle? { 992 | return if (mStartHandle!!.isLeft == isLeft) { 993 | mStartHandle 994 | } else { 995 | mEndHandle 996 | } 997 | } 998 | 999 | private inner class SelectionInfo { 1000 | var mStart = 0 1001 | var mEnd = 0 1002 | var mSelectionContent: CharSequence? = null 1003 | } 1004 | 1005 | /** 1006 | * 处理内容链接跳转 1007 | */ 1008 | private inner class LinkMovementMethodInterceptor : LinkMovementMethod() { 1009 | private var downLinkTime: Long = 0 1010 | override fun onTouchEvent( 1011 | widget: TextView, 1012 | buffer: Spannable, 1013 | event: MotionEvent 1014 | ): Boolean { 1015 | val action = event.action 1016 | if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { 1017 | var x = event.x.toInt() 1018 | var y = event.y.toInt() 1019 | x -= widget.totalPaddingLeft 1020 | y -= widget.totalPaddingTop 1021 | x += widget.scrollX 1022 | y += widget.scrollY 1023 | val layout = widget.layout 1024 | val line = layout.getLineForVertical(y) 1025 | val off = layout.getOffsetForHorizontal(line, x.toFloat()) 1026 | val links = buffer.getSpans(off, off, ClickableSpan::class.java) 1027 | if (links.isNotEmpty()) { 1028 | if (action == MotionEvent.ACTION_UP) { // 长按 1029 | if (downLinkTime + ViewConfiguration.getLongPressTimeout() < System.currentTimeMillis()) { 1030 | return false 1031 | } // 点击 1032 | if (links[0] is URLSpan) { 1033 | val url = links[0] as URLSpan 1034 | if (!TextUtils.isEmpty(url.url)) { 1035 | if (null != mSelectListener) { 1036 | usedClickListener = true 1037 | mSelectListener!!.onClickUrl(url.url) 1038 | } 1039 | return true 1040 | } else { 1041 | links[0].onClick(widget) 1042 | } 1043 | } 1044 | } else if (action == MotionEvent.ACTION_DOWN) { 1045 | downLinkTime = System.currentTimeMillis() 1046 | Selection.setSelection( 1047 | buffer, buffer.getSpanStart(links[0]), buffer.getSpanEnd(links[0]) 1048 | ) 1049 | } 1050 | return true 1051 | } else { 1052 | Selection.removeSelection(buffer) 1053 | } 1054 | } 1055 | return super.onTouchEvent(widget, buffer, event) 1056 | } 1057 | } 1058 | 1059 | } 1060 | --------------------------------------------------------------------------------