From 84181ccb7e5575697194385f2a5a73cda163e194 Mon Sep 17 00:00:00 2001 From: itgaojian163 Date: Tue, 29 Oct 2024 21:39:38 +0800 Subject: [PATCH] =?UTF-8?q?whellview=EF=BC=8C=E6=9D=83=E9=99=90=E7=94=B3?= =?UTF-8?q?=E8=AF=B7=EF=BC=8Cemoji?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/compiler.xml | 2 +- .idea/misc.xml | 2 +- .idea/runConfigurations.xml | 17 + .../aimz_k/page/activity/ChatActivity.kt | 39 +- app/src/main/res/layout/activity_chat.xml | 213 +- baselib/build.gradle.kts | 4 +- .../widget/wheel/CurvedArcDirection.kt | 15 + .../baselib/widget/wheel/DividerType.kt | 11 + .../baselib/widget/wheel/TextAlign.kt | 10 + .../baselib/widget/wheel/WheelView.kt | 2645 +++++++++++++++++ baselib/src/main/res/values/attrs.xml | 67 + gradle/libs.versions.toml | 11 +- settings.gradle.kts | 1 + 13 files changed, 2968 insertions(+), 69 deletions(-) create mode 100644 .idea/runConfigurations.xml create mode 100644 baselib/src/main/java/com/tenlionsoft/baselib/widget/wheel/CurvedArcDirection.kt create mode 100644 baselib/src/main/java/com/tenlionsoft/baselib/widget/wheel/DividerType.kt create mode 100644 baselib/src/main/java/com/tenlionsoft/baselib/widget/wheel/TextAlign.kt create mode 100644 baselib/src/main/java/com/tenlionsoft/baselib/widget/wheel/WheelView.kt diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b589d56..b86273d 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3223143..90b9ca7 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - + diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/tenlionsoft/aimz_k/page/activity/ChatActivity.kt b/app/src/main/java/com/tenlionsoft/aimz_k/page/activity/ChatActivity.kt index c4ddc99..7ae1147 100644 --- a/app/src/main/java/com/tenlionsoft/aimz_k/page/activity/ChatActivity.kt +++ b/app/src/main/java/com/tenlionsoft/aimz_k/page/activity/ChatActivity.kt @@ -5,6 +5,8 @@ import android.view.WindowManager import androidx.core.content.ContextCompat import androidx.databinding.DataBindingUtil import androidx.lifecycle.ViewModelProvider +import com.hjq.permissions.OnPermissionCallback +import com.hjq.permissions.XXPermissions import com.tenlionsoft.aimz_k.R import com.tenlionsoft.aimz_k.databinding.ActivityChatBinding import com.tenlionsoft.aimz_k.viewmodel.ChatPageViewModel @@ -32,5 +34,40 @@ class ChatActivity : BaseActivity() { chatPageViewModel.showSendBtn.observe(this) { mBinding.ivSend.visibility = if (it) View.GONE else View.VISIBLE } + mBinding.rpvEmoji.setOnEmojiPickedListener { + mBinding.etMsg.append(it.emoji) + } } -} \ No newline at end of file +} + + + +//XXPermissions.with(this) +//// 申请单个权限 +//.permission(Permission.RECORD_AUDIO) +//// 申请多个权限 +//.permission(Permission.Group.CALENDAR) +//// 设置权限请求拦截器(局部设置) +////.interceptor(new PermissionInterceptor()) +//// 设置不触发错误检测机制(局部设置) +////.unchecked() +//.request(object : OnPermissionCallback { +// +// override fun onGranted(permissions: MutableList, allGranted: Boolean) { +// if (!allGranted) { +// toast("获取部分权限成功,但部分权限未正常授予") +// return +// } +// toast("获取录音和日历权限成功") +// } +// +// override fun onDenied(permissions: MutableList, doNotAskAgain: Boolean) { +// if (doNotAskAgain) { +// toast("被永久拒绝授权,请手动授予录音和日历权限") +// // 如果是被永久拒绝就跳转到应用权限系统设置页面 +// XXPermissions.startPermissionActivity(context, permissions) +// } else { +// toast("获取录音和日历权限失败") +// } +// } +//}) \ No newline at end of file diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml index 6618220..3d83575 100644 --- a/app/src/main/res/layout/activity_chat.xml +++ b/app/src/main/res/layout/activity_chat.xml @@ -80,75 +80,172 @@ + android:background="@color/input_layout_bg" + android:gravity="center_vertical" + android:minHeight="50dp" + android:orientation="horizontal" + android:paddingLeft="10dp" + android:paddingTop="5dp" + android:paddingRight="10dp" + android:paddingBottom="5dp"> + + + + + + + + + + + + + + + + + + + + + - - + android:layout_height="match_parent" + android:orientation="vertical" + android:visibility="gone"> + android:layout_width="match_parent" + android:layout_height="wrap_content"> - + android:layout_weight="1"> + + + + + + + + + + + + + + + + + + + - - - - - - + \ No newline at end of file diff --git a/baselib/build.gradle.kts b/baselib/build.gradle.kts index d7313bf..2b356ba 100644 --- a/baselib/build.gradle.kts +++ b/baselib/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation(libs.androidx.core.ktx) api(libs.androidx.appcompat) implementation(libs.material) + implementation(libs.androidx.emoji2.emojipicker) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) @@ -53,7 +54,6 @@ dependencies { api(libs.lifecycleViewModel) api(libs.rxkotlin) api(libs.glide) - api(libs.permissionX) api(libs.retrofit) api(libs.rxjava) @@ -66,4 +66,6 @@ dependencies { api(libs.refreshHeader) api(libs.refreshFoot) api(libs.jjwt) + api(libs.androidx.emoji2.emojipicker) + api(libs.xxpermissions) } \ No newline at end of file diff --git a/baselib/src/main/java/com/tenlionsoft/baselib/widget/wheel/CurvedArcDirection.kt b/baselib/src/main/java/com/tenlionsoft/baselib/widget/wheel/CurvedArcDirection.kt new file mode 100644 index 0000000..303aa90 --- /dev/null +++ b/baselib/src/main/java/com/tenlionsoft/baselib/widget/wheel/CurvedArcDirection.kt @@ -0,0 +1,15 @@ +package com.tenlionsoft.baselib.widget.wheel + +import androidx.annotation.IntDef + + +/** + * 左右圆弧效果方向注解 + */ +@IntDef( + WheelView.CURVED_ARC_DIRECTION_LEFT, + WheelView.CURVED_ARC_DIRECTION_CENTER, + WheelView.CURVED_ARC_DIRECTION_RIGHT +) +@Retention(AnnotationRetention.SOURCE) +annotation class CurvedArcDirection diff --git a/baselib/src/main/java/com/tenlionsoft/baselib/widget/wheel/DividerType.kt b/baselib/src/main/java/com/tenlionsoft/baselib/widget/wheel/DividerType.kt new file mode 100644 index 0000000..d12242f --- /dev/null +++ b/baselib/src/main/java/com/tenlionsoft/baselib/widget/wheel/DividerType.kt @@ -0,0 +1,11 @@ +package com.tenlionsoft.baselib.widget.wheel + +import androidx.annotation.IntDef + + +/** + * 分割线类型注解 + */ +@IntDef(WheelView.DIVIDER_TYPE_FILL, WheelView.DIVIDER_TYPE_WRAP) +@Retention(AnnotationRetention.SOURCE) +annotation class DividerType diff --git a/baselib/src/main/java/com/tenlionsoft/baselib/widget/wheel/TextAlign.kt b/baselib/src/main/java/com/tenlionsoft/baselib/widget/wheel/TextAlign.kt new file mode 100644 index 0000000..382dde5 --- /dev/null +++ b/baselib/src/main/java/com/tenlionsoft/baselib/widget/wheel/TextAlign.kt @@ -0,0 +1,10 @@ +package com.tenlionsoft.baselib.widget.wheel + +import androidx.annotation.IntDef + +/** + * 文字对齐方式注解 + */ +@IntDef(WheelView.TEXT_ALIGN_LEFT, WheelView.TEXT_ALIGN_CENTER, WheelView.TEXT_ALIGN_RIGHT) +@Retention(AnnotationRetention.SOURCE) +annotation class TextAlign \ No newline at end of file diff --git a/baselib/src/main/java/com/tenlionsoft/baselib/widget/wheel/WheelView.kt b/baselib/src/main/java/com/tenlionsoft/baselib/widget/wheel/WheelView.kt new file mode 100644 index 0000000..7e3face --- /dev/null +++ b/baselib/src/main/java/com/tenlionsoft/baselib/widget/wheel/WheelView.kt @@ -0,0 +1,2645 @@ +package com.tenlionsoft.baselib.widget.wheel + +import android.content.Context +import android.content.res.Resources +import android.graphics.Camera +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Typeface +import android.media.AudioManager +import android.media.SoundPool +import android.os.Build +import android.util.AttributeSet +import android.util.TypedValue +import android.view.MotionEvent +import android.view.VelocityTracker +import android.view.View +import android.view.ViewConfiguration +import android.widget.OverScroller +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.FloatRange +import androidx.annotation.RawRes +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import com.tenlionsoft.baselib.R +import java.util.Locale +import kotlin.math.abs +import kotlin.math.cos +import kotlin.math.sin + +open class WheelView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr), Runnable { + + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + + /** + * 字体大小 + */ + private var textSize: Float = 0F + + /** + * 是否自动调整字体大小以显示完全 + */ + private var isAutoFitTextSize: Boolean = false + + private var fontMetrics: Paint.FontMetrics? = null + + /** + * 每个item的高度 + */ + private var itemHeight: Int = 0 + + /** + * 文字的最大宽度 + */ + private var maxTextWidth: Int = 0 + + /** + * 文字中心距离baseline的距离 + */ + private var centerToBaselineY: Int = 0 + + /** + * 可见的item条数 + */ + private var visibleItems: Int = 0 + + /** + * 每个item之间的空间,行间距 + */ + private var lineSpacing: Float = 0F + + /** + * 是否循环滚动 + */ + private var isCyclic: Boolean = false + + /** + * 文字对齐方式 + */ + @TextAlign + private var textAlign: Int = 0 + + /** + * 文字颜色 + */ + private var textColor: Int = 0 + + /** + * 选中item文字颜色 + */ + private var selectedItemTextColor: Int = 0 + + /** + * 是否显示分割线 + */ + private var isShowDivider: Boolean = false + + /** + * 分割线的颜色 + */ + private var dividerColor: Int = 0 + + /** + * 分割线高度 + */ + private var dividerSize: Float = 0F + + /** + * 分割线填充类型 + */ + @DividerType + private var dividerType: Int = 0 + + /** + * 分割线类型为DIVIDER_TYPE_WRAP时 分割线左右两端距离文字的间距 + */ + private var dividerPaddingForWrap: Float = 0.toFloat() + + /** + * 分割线两端形状,默认圆头 + */ + private var dividerCap: Paint.Cap = Paint.Cap.ROUND + + /** + * 是否绘制选中区域 + */ + private var isDrawSelectedRect: Boolean = false + + /** + * 选中区域背景颜色 + */ + private var selectedRectColor: Int = 0 + + /** + * 选中区域背景左侧圆角 + */ + private var selectedRectLeftRadius: Float = 0F + + /** + * 选中区域背景右侧圆角 + */ + private var selectedRectRightRadius: Float = 0F + + /** + * 文字起始X + */ + private var startX: Int = 0 + + /** + * X轴中心点 + */ + private var centerX: Int = 0 + + /** + * Y轴中心点 + */ + private var centerY: Int = 0 + + /** + * 选中边界的上下限制 + */ + private var selectedItemTopLimit: Int = 0 + + private var selectedItemBottomLimit: Int = 0 + + /** + * 裁剪边界 + */ + private var clipLeft: Int = 0 + + private var clipTop: Int = 0 + + private var clipRight: Int = 0 + + private var clipBottom: Int = 0 + + /** + * 绘制区域 + */ + private val drawRect = Rect() + + /** + * 字体外边距,目的是留有边距 + */ + private var textBoundaryMargin: Float = 0F + + /** + * 数据为Integer类型时,是否需要格式转换 + */ + private var isIntegerNeedFormat: Boolean = false + + /** + * 数据为Integer类型时,转换格式,默认转换为两位数 + */ + private var integerFormat: String = DEFAULT_INTEGER_FORMAT + + /** + * 3D效果 + */ + private var camera = Camera() + + private var mMatrix = Matrix() + + /** + * 是否是弯曲(3D)效果 + */ + private var isCurved: Boolean = false + + /** + * 弯曲(3D)效果左右圆弧偏移效果方向 center 不偏移 + */ + @CurvedArcDirection + private var curvedArcDirection: Int = 0 + + /** + * 弯曲(3D)效果左右圆弧偏移效果系数 0-1之间 越大越明显 + */ + private var curvedArcDirectionFactor: Float = 0F + + /** + * 弯曲(3D)效果选中后折射的偏移 与字体大小的比值,1为不偏移 越小偏移越明显 + */ + private var curvedRefractRatio: Float = 0F + + /** + * 数据列表 + */ + private var dataItems: MutableList = mutableListOf() + + /** + * 不活跃的下标,选中时不变色 + */ + private var inactiveIndexes: MutableList = mutableListOf() + + /** + * 数据变化时,是否重置选中下标到第一个位置 + */ + private var isResetSelectedPosition = false + + private var velocityTracker: VelocityTracker? = null + + private var maxFlingVelocity: Int = 0 + + private var minFlingVelocity: Int = 0 + + private var overScroller = OverScroller(context) + + /** + * 最小滚动距离,上边界 + */ + private var minScrollY: Int = 0 + + /** + * 最大滚动距离,下边界 + */ + private var maxScrollY: Int = 0 + + /** + * Y轴滚动偏移 + */ + private var scrollOffsetY: Int = 0 + + /** + * Y轴已滚动偏移,控制重绘次数 + */ + private var scrolledY = 0 + + /** + * 手指最后触摸的位置 + */ + private var lastTouchY: Float = 0F + + /** + * 手指按下时间,根据按下抬起时间差处理点击滚动 + */ + private var downStartTime: Long = 0 + + /** + * 是否强制停止滚动 + */ + private var isForceFinishScroll = false + + /** + * 是否是快速滚动,快速滚动结束后跳转位置 + */ + private var isFlingScroll: Boolean = false + + /** + * 当前选中的下标 + */ + private var selectedItemPosition: Int = 0 + + /** + * 当前滚动经过的下标 + */ + private var currentScrollPosition: Int = 0 + + /** + * 选择监听器 + */ + private var onItemSelectedListener: OnItemSelectedListener? = null + + /** + * 滚动变化监听 + */ + private var onWheelChangedListener: OnWheelChangedListener? = null + + /** + * 音频 + */ + private var soundHelper = SoundHelper() + + /** + * 是否开启音频效果 + */ + private var isSoundEffect = false + + private var selectItemPosition = 0 + + private var isSmoothScroll = false + + init { + initAttrsAndDefault(context, attrs) + initValue(context) + } + + /** + * 初始化自定义属性及默认值 + * + * @param context 上下文 + * @param attrs attrs + */ + private fun initAttrsAndDefault(context: Context, attrs: AttributeSet?) { + val ta = context.obtainStyledAttributes(attrs, R.styleable.WheelView) + textSize = ta.getDimension(R.styleable.WheelView_wv_textSize, DEFAULT_TEXT_SIZE) + isAutoFitTextSize = ta.getBoolean(R.styleable.WheelView_wv_autoFitTextSize, false) + textAlign = ta.getInt(R.styleable.WheelView_wv_textAlign, TEXT_ALIGN_CENTER) + textBoundaryMargin = ta.getDimension( + R.styleable.WheelView_wv_textBoundaryMargin, + DEFAULT_TEXT_BOUNDARY_MARGIN + ) + textColor = + ta.getColor(R.styleable.WheelView_wv_normalItemTextColor, DEFAULT_NORMAL_TEXT_COLOR) + selectedItemTextColor = + ta.getColor(R.styleable.WheelView_wv_selectedItemTextColor, DEFAULT_SELECTED_TEXT_COLOR) + lineSpacing = ta.getDimension(R.styleable.WheelView_wv_lineSpacing, DEFAULT_LINE_SPACING) + isIntegerNeedFormat = ta.getBoolean(R.styleable.WheelView_wv_integerNeedFormat, false) + integerFormat = + ta.getString(R.styleable.WheelView_wv_integerFormat) ?: DEFAULT_INTEGER_FORMAT + visibleItems = ta.getInt(R.styleable.WheelView_wv_visibleItems, DEFAULT_VISIBLE_ITEM) + // 跳转可见item为奇数 + visibleItems = adjustVisibleItems(visibleItems) + selectedItemPosition = ta.getInt(R.styleable.WheelView_wv_selectedItemPosition, 0) + // 初始化滚动下标 + currentScrollPosition = selectedItemPosition + isCyclic = ta.getBoolean(R.styleable.WheelView_wv_cyclic, false) + + isShowDivider = ta.getBoolean(R.styleable.WheelView_wv_showDivider, false) + dividerType = ta.getInt(R.styleable.WheelView_wv_dividerType, DIVIDER_TYPE_FILL) + dividerSize = + ta.getDimension(R.styleable.WheelView_wv_dividerHeight, DEFAULT_DIVIDER_HEIGHT) + dividerColor = + ta.getColor(R.styleable.WheelView_wv_dividerColor, DEFAULT_SELECTED_TEXT_COLOR) + this.dividerPaddingForWrap = ta.getDimension( + R.styleable.WheelView_wv_dividerPaddingForWrap, + DEFAULT_TEXT_BOUNDARY_MARGIN + ) + + isDrawSelectedRect = ta.getBoolean(R.styleable.WheelView_wv_drawSelectedRect, false) + selectedRectColor = + ta.getColor(R.styleable.WheelView_wv_selectedRectColor, Color.TRANSPARENT) + val selectedRectRadius = ta.getDimension(R.styleable.WheelView_wv_selectedRectRadius, 0F) + selectedRectLeftRadius = + ta.getDimension(R.styleable.WheelView_wv_selectedRectLeftRadius, selectedRectRadius) + selectedRectRightRadius = + ta.getDimension(R.styleable.WheelView_wv_selectedRectRightRadius, selectedRectRadius) + + isCurved = ta.getBoolean(R.styleable.WheelView_wv_curved, true) + curvedArcDirection = + ta.getInt(R.styleable.WheelView_wv_curvedArcDirection, CURVED_ARC_DIRECTION_CENTER) + curvedArcDirectionFactor = + ta.getFloat(R.styleable.WheelView_wv_curvedArcDirectionFactor, DEFAULT_CURVED_FACTOR) + // 折射偏移默认值 + curvedRefractRatio = + ta.getFloat(R.styleable.WheelView_wv_curvedRefractRatio, DEFAULT_REFRACT_RATIO) + if (curvedRefractRatio > 1f) { + curvedRefractRatio = 1.0f + } else if (curvedRefractRatio < 0f) { + curvedRefractRatio = DEFAULT_REFRACT_RATIO + } + ta.recycle() + } + + /** + * 初始化并设置默认值 + * + * @param context 上下文 + */ + private fun initValue(context: Context) { + val viewConfiguration = ViewConfiguration.get(context) + maxFlingVelocity = viewConfiguration.scaledMaximumFlingVelocity + minFlingVelocity = viewConfiguration.scaledMinimumFlingVelocity + initDefaultVolume(context) + calculateTextSize() + updateTextAlign() + } + + /** + * 获取文字对齐方式 + * + * @return 文字对齐 [TEXT_ALIGN_LEFT] [TEXT_ALIGN_CENTER] [TEXT_ALIGN_RIGHT] + */ + fun getTextAlign(): Int { + return textAlign + } + + /** + * 设置文字对齐方式 + * + * @param textAlign 文字对齐方式 [TEXT_ALIGN_LEFT] [TEXT_ALIGN_CENTER] [TEXT_ALIGN_RIGHT] + */ + fun setTextAlign(@TextAlign textAlign: Int) { + if (this.textAlign == textAlign) { + return + } + this.textAlign = textAlign + updateTextAlign() + calculateDrawStart() + invalidate() + } + + /** + * 获取未选中条目颜色 + * + * @return 未选中条目颜色 ColorInt + */ + fun getNormalItemTextColor(): Int { + return textColor + } + + /** + * 设置未选中条目颜色 + * + * @param textColor 未选中条目颜色 [ColorInt] + */ + fun setNormalItemTextColor(@ColorInt textColor: Int) { + if (this.textColor == textColor) { + return + } + this.textColor = textColor + invalidate() + } + + /** + * 设置未选中条目颜色 + * + * @param textColorRes 未选中条目颜色 [ColorRes] + */ + fun setNormalItemTextColorRes(@ColorRes textColorRes: Int) { + setNormalItemTextColor(ContextCompat.getColor(context, textColorRes)) + } + + /** + * 获取选中条目颜色 + * + * @return 选中条目颜色 ColorInt + */ + fun getSelectedItemTextColor(): Int { + return selectedItemTextColor + } + + /** + * 设置选中条目颜色 + * + * @param selectedItemTextColor 选中条目颜色 [ColorInt] + */ + fun setSelectedItemTextColor(@ColorInt selectedItemTextColor: Int) { + if (this.selectedItemTextColor == selectedItemTextColor) { + return + } + this.selectedItemTextColor = selectedItemTextColor + invalidate() + } + + /** + * 设置选中条目颜色 + * + * @param selectedItemColorRes 选中条目颜色 [ColorRes] + */ + fun setSelectedItemTextColorRes(@ColorRes selectedItemColorRes: Int) { + setSelectedItemTextColor(ContextCompat.getColor(context, selectedItemColorRes)) + } + + /** + * 获取文字距离边界的外边距 + * + * @return 外边距值 + */ + fun getTextBoundaryMargin(): Float { + return textBoundaryMargin + } + + /** + * 设置文字距离边界的外边距 + * + * @param textBoundaryMargin 外边距值 + */ + fun setTextBoundaryMargin(textBoundaryMargin: Float) { + setTextBoundaryMargin(textBoundaryMargin, false) + } + + /** + * 设置文字距离边界的外边距 + * + * @param textBoundaryMargin 外边距值 + * @param isDp 单位是否为 dp + */ + fun setTextBoundaryMargin(textBoundaryMargin: Float, isDp: Boolean) { + val tempTextBoundaryMargin = this.textBoundaryMargin + this.textBoundaryMargin = if (isDp) dp2px(textBoundaryMargin) else textBoundaryMargin + if (tempTextBoundaryMargin == this.textBoundaryMargin) { + return + } + requestLayout() + invalidate() + } + + /** + * 获取Integer类型转换格式 + * + * @return integerFormat + */ + fun getIntegerFormat(): String { + return integerFormat + } + + /** + * 设置Integer类型转换格式 + * + * @param integerFormat + */ + fun setIntegerFormat(integerFormat: String) { + if (integerFormat.isEmpty() || integerFormat == this.integerFormat) { + return + } + this.integerFormat = integerFormat + calculateTextSize() + requestLayout() + invalidate() + } + + /** + * 获取可见条目数 + * + * @return 可见条目数 + */ + fun getVisibleItems(): Int { + return visibleItems + } + + /** + * 设置可见的条目数 + * + * @param visibleItems 可见条目数 + */ + fun setVisibleItems(visibleItems: Int) { + if (this.visibleItems == visibleItems) { + return + } + this.visibleItems = adjustVisibleItems(visibleItems) + scrollOffsetY = 0 + requestLayout() + invalidate() + } + + /** + * 获取当前选中下标 + * + * @return 当前选中的下标 + */ + fun getSelectedItemPosition(): Int { + return selectedItemPosition + } + + /** + * 获取分割线颜色 + * + * @return 分割线颜色 ColorInt + */ + fun getDividerColor(): Int { + return dividerColor + } + + /** + * 设置分割线颜色 + * + * @param dividerColor 分割线颜色 [ColorInt] + */ + fun setDividerColor(@ColorInt dividerColor: Int) { + if (this.dividerColor == dividerColor) { + return + } + this.dividerColor = dividerColor + invalidate() + } + + /** + * 设置分割线颜色 + * + * @param dividerColorRes 分割线颜色 [ColorRes] + */ + fun setDividerColorRes(@ColorRes dividerColorRes: Int) { + setDividerColor(ContextCompat.getColor(context, dividerColorRes)) + } + + /** + * 获取分割线高度 + * + * @return 分割线高度 + */ + fun getDividerHeight(): Float { + return dividerSize + } + + /** + * 设置分割线高度 + * + * @param dividerHeight 分割线高度 + */ + fun setDividerHeight(dividerHeight: Float) { + setDividerHeight(dividerHeight, false) + } + + /** + * 设置分割线高度 + * + * @param dividerHeight 分割线高度 + * @param isDp 单位是否是 dp + */ + fun setDividerHeight(dividerHeight: Float, isDp: Boolean) { + val tempDividerHeight = dividerSize + dividerSize = if (isDp) dp2px(dividerHeight) else dividerHeight + if (tempDividerHeight == dividerSize) { + return + } + invalidate() + } + + /** + * 获取分割线填充类型 + * + * @return 分割线填充类型 [DIVIDER_TYPE_FILL] [DIVIDER_TYPE_WRAP] + */ + fun getDividerType(): Int { + return dividerType + } + + /** + * 设置分割线填充类型 + * + * @param dividerType 分割线填充类型 [DIVIDER_TYPE_FILL] [DIVIDER_TYPE_WRAP] + */ + fun setDividerType(@DividerType dividerType: Int) { + if (this.dividerType == dividerType) { + return + } + this.dividerType = dividerType + invalidate() + } + + /** + * 获取自适应分割线类型时的分割线内边距 + * + * @return 分割线内边距 + */ + fun getDividerPaddingForWrap(): Float { + return dividerPaddingForWrap + } + + /** + * 设置自适应分割线类型时的分割线内边距 + * + * @param dividerPaddingForWrap 分割线内边距 + */ + fun setDividerPaddingForWrap(dividerPaddingForWrap: Float) { + setDividerPaddingForWrap(dividerPaddingForWrap, false) + } + + /** + * 设置自适应分割线类型时的分割线内边距 + * + * @param dividerPaddingForWrap 分割线内边距 + * @param isDp 单位是否是 dp + */ + fun setDividerPaddingForWrap(dividerPaddingForWrap: Float, isDp: Boolean) { + val tempDividerPadding = this.dividerPaddingForWrap + this.dividerPaddingForWrap = + if (isDp) dp2px(dividerPaddingForWrap) else dividerPaddingForWrap + if (tempDividerPadding == this.dividerPaddingForWrap) { + return + } + invalidate() + } + + /** + * 获取分割线两端形状 + * + * @return 分割线两端形状 [Paint.Cap.BUTT] [Paint.Cap.ROUND] [Paint.Cap.SQUARE] + */ + fun getDividerCap(): Paint.Cap { + return dividerCap + } + + /** + * 设置分割线两端形状 + * + * @param dividerCap 分割线两端形状 [Paint.Cap.BUTT] [Paint.Cap.ROUND] [Paint.Cap.SQUARE] + */ + fun setDividerCap(dividerCap: Paint.Cap) { + if (this.dividerCap == dividerCap) { + return + } + this.dividerCap = dividerCap + invalidate() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + soundHelper.release() + } + + /** + * 初始化默认音量 + * + * @param context 上下文 + */ + private fun initDefaultVolume(context: Context) { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) + if (audioManager != null) { + audioManager as AudioManager + // 获取系统媒体当前音量 + val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + // 获取系统媒体最大音量 + val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) + // 设置播放音量 + soundHelper.playVolume = currentVolume * 1.0f / maxVolume + } else { + soundHelper.playVolume = 0.3f + } + } + + /** + * 测量文字最大所占空间 + */ + private fun calculateTextSize() { + paint.textSize = textSize + for (i in dataItems.indices) { + val textWidth = paint.measureText(getDataText(dataItems[i])).toInt() + maxTextWidth = textWidth.coerceAtLeast(maxTextWidth) + } + + this.fontMetrics = paint.fontMetrics + // itemHeight实际等于字体高度+一个行间距 + this.itemHeight = (fontMetrics!!.bottom - fontMetrics!!.top + lineSpacing).toInt() + } + + /** + * 更新textAlign + */ + private fun updateTextAlign() { + when (textAlign) { + TEXT_ALIGN_LEFT -> paint.textAlign = Paint.Align.LEFT + TEXT_ALIGN_RIGHT -> paint.textAlign = Paint.Align.RIGHT + else -> paint.textAlign = Paint.Align.CENTER + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + // Line Space算在了itemHeight中 + val height: Int = if (isCurved) { + (this.itemHeight * visibleItems * 2 / Math.PI + paddingTop.toDouble() + paddingBottom.toDouble()).toInt() + } else { + this.itemHeight * visibleItems + paddingTop + paddingBottom + } + var width = + (maxTextWidth.toFloat() + paddingLeft.toFloat() + paddingRight.toFloat() + textBoundaryMargin * 2).toInt() + if (isCurved) { + val towardRange = (sin(Math.PI / 48) * height).toInt() + width += towardRange + } + setMeasuredDimension( + resolveSizeAndState(width, widthMeasureSpec, 0), + resolveSizeAndState(height, heightMeasureSpec, 0) + ) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + // 设置内容可绘制区域 + drawRect.set(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom) + centerX = drawRect.centerX() + this.centerY = drawRect.centerY() + selectedItemTopLimit = centerY - itemHeight / 2 + selectedItemBottomLimit = centerY + itemHeight / 2 + clipLeft = paddingLeft + clipTop = paddingTop + clipRight = width - paddingRight + clipBottom = height - paddingBottom + + calculateDrawStart() + // 计算滚动限制 + calculateLimitY() + } + + /** + * 起算起始位置 + */ + private fun calculateDrawStart() { + startX = when (textAlign) { + TEXT_ALIGN_LEFT -> (paddingLeft + textBoundaryMargin).toInt() + TEXT_ALIGN_RIGHT -> (width - paddingRight - textBoundaryMargin).toInt() + else -> width / 2 + } + + // 文字中心距离baseline的距离 + centerToBaselineY = + (fontMetrics!!.ascent + (fontMetrics!!.descent - fontMetrics!!.ascent) / 2).toInt() + } + + /** + * 计算滚动限制 + */ + private fun calculateLimitY() { + minScrollY = if (isCyclic) Integer.MIN_VALUE else 0 + // 下边界 (dataItemsSize - 1 - mInitPosition) * itemHeight + maxScrollY = if (isCyclic) Integer.MAX_VALUE else (dataItems.size - 1) * this.itemHeight + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + // 绘制选中区域 + drawSelectedRect(canvas) + // 绘制分割线 + drawDivider(canvas) + + // 滚动了多少个item,滚动的Y值高度除去每行Item的高度 + val scrolledItem = scrollOffsetY / itemHeight + // 没有滚动完一个item时的偏移值,平滑滑动 + val scrolledOffset = scrollOffsetY % itemHeight + // 向上取整 + val halfItem = (visibleItems + 1) / 2 + // 计算的最小index + val minIndex: Int + // 计算的最大index + val maxIndex: Int + when { + scrolledOffset < 0 -> { + // 小于0 + minIndex = scrolledItem - halfItem - 1 + maxIndex = scrolledItem + halfItem + } + + scrolledOffset > 0 -> { + minIndex = scrolledItem - halfItem + maxIndex = scrolledItem + halfItem + 1 + } + + else -> { + minIndex = scrolledItem - halfItem + maxIndex = scrolledItem + halfItem + } + } + + // 绘制item + for (i in minIndex until maxIndex) { + if (isCurved) { + draw3DItem(canvas, i, scrolledOffset) + } else { + drawItem(canvas, i, scrolledOffset) + } + } + + } + + /** + * 绘制选中区域 + * + * @param canvas 画布 + */ + private fun drawSelectedRect(canvas: Canvas) { + if (!isDrawSelectedRect) { + return + } + + paint.color = selectedRectColor + + val path = Path() + path.moveTo(clipLeft + selectedRectLeftRadius, selectedItemTopLimit.toFloat()) + // 上边 + path.lineTo(clipRight - selectedRectLeftRadius, selectedItemTopLimit.toFloat()) + // 右上角 + val rightTopCircleRect = RectF( + clipRight - selectedRectRightRadius * 2, + selectedItemTopLimit.toFloat(), + clipRight.toFloat(), + selectedItemTopLimit + selectedRectRightRadius * 2 + ) + path.arcTo(rightTopCircleRect, 270f, 90f) + // 右边 + path.lineTo(clipRight.toFloat(), selectedItemBottomLimit - selectedRectRightRadius) + // 右下角 + val rightBottomCircleRect = RectF( + clipRight - selectedRectRightRadius * 2, + selectedItemBottomLimit - selectedRectRightRadius * 2, + clipRight.toFloat(), + selectedItemBottomLimit.toFloat() + ) + path.arcTo(rightBottomCircleRect, 0f, 90f) + // 下边 + path.lineTo(clipLeft + selectedRectLeftRadius, selectedItemBottomLimit.toFloat()) + // 左下角 + val leftBottomCircleRect = RectF( + clipLeft.toFloat(), + selectedItemBottomLimit - selectedRectLeftRadius * 2, + clipLeft + selectedRectLeftRadius * 2, + selectedItemBottomLimit.toFloat() + ) + path.arcTo(leftBottomCircleRect, 90f, 90f) + // 左边 + path.lineTo(clipLeft.toFloat(), selectedItemTopLimit + selectedRectLeftRadius) + // 左上角 + val leftTopCircleRect = RectF( + clipLeft.toFloat(), + selectedItemTopLimit.toFloat(), + clipLeft + selectedRectLeftRadius * 2, + selectedItemTopLimit + selectedRectLeftRadius * 2 + ) + path.arcTo(leftTopCircleRect, 180f, 90f) + + path.close() + canvas.drawPath(path, paint) + } + + /** + * 绘制分割线 + * + * @param canvas 画布 + */ + private fun drawDivider(canvas: Canvas) { + if (isShowDivider) { + paint.color = dividerColor + val originStrokeWidth = paint.strokeWidth + paint.strokeJoin = Paint.Join.ROUND + paint.strokeCap = Paint.Cap.ROUND + paint.strokeWidth = dividerSize + if (dividerType == DIVIDER_TYPE_FILL) { + canvas.drawLine( + clipLeft.toFloat(), + selectedItemTopLimit.toFloat(), + clipRight.toFloat(), + selectedItemTopLimit.toFloat(), + paint + ) + canvas.drawLine( + clipLeft.toFloat(), + selectedItemBottomLimit.toFloat(), + clipRight.toFloat(), + selectedItemBottomLimit.toFloat(), + paint + ) + } else { + // 边界处理 超过边界直接按照DIVIDER_TYPE_FILL类型处理 + val startX = + (centerX.toFloat() - (maxTextWidth / 2).toFloat() - dividerPaddingForWrap).toInt() + val stopX = + (this.centerX.toFloat() + (maxTextWidth / 2).toFloat() + dividerPaddingForWrap).toInt() + + val wrapStartX = if (startX < clipLeft) clipLeft else startX + val wrapStopX = if (stopX > clipRight) clipRight else stopX + canvas.drawLine( + wrapStartX.toFloat(), + selectedItemTopLimit.toFloat(), + wrapStopX.toFloat(), + selectedItemTopLimit.toFloat(), + paint + ) + canvas.drawLine( + wrapStartX.toFloat(), + selectedItemBottomLimit.toFloat(), + wrapStopX.toFloat(), + selectedItemBottomLimit.toFloat(), + paint + ) + } + paint.strokeWidth = originStrokeWidth + } + } + + /** + * 绘制2D效果 + * + * @param canvas 画布 + * @param index 下标 + * @param scrolledOffset 滚动偏移 + */ + private fun drawItem(canvas: Canvas, index: Int, scrolledOffset: Int) { + val text = getDataByIndex(index) ?: return + + // index 的 item 距离中间项的偏移 + val item2CenterOffsetY = + (index - scrollOffsetY / this.itemHeight) * this.itemHeight - scrolledOffset + // 记录初始测量的字体起始X + val startX = startX + // 重新测量字体宽度和基线偏移 + val centerToBaselineY = + if (isAutoFitTextSize) remeasureTextSize(text) else centerToBaselineY + + if (abs(item2CenterOffsetY) <= 0) { + // 绘制选中的条目 + paint.color = if (isInactive(index)) textColor else selectedItemTextColor + clipAndDraw2DText( + canvas, + text, + selectedItemTopLimit, + selectedItemBottomLimit, + item2CenterOffsetY, + centerToBaselineY + ) + } else if (item2CenterOffsetY > 0 && item2CenterOffsetY < this.itemHeight) { + // 绘制与下边界交汇的条目 + paint.color = if (isInactive(index)) textColor else selectedItemTextColor + clipAndDraw2DText( + canvas, + text, + selectedItemTopLimit, + selectedItemBottomLimit, + item2CenterOffsetY, + centerToBaselineY + ) + + paint.color = textColor + clipAndDraw2DText( + canvas, + text, + selectedItemBottomLimit, + clipBottom, + item2CenterOffsetY, + centerToBaselineY + ) + + } else if (item2CenterOffsetY < 0 && item2CenterOffsetY > -this.itemHeight) { + // 绘制与上边界交汇的条目 + paint.color = if (isInactive(index)) textColor else selectedItemTextColor + clipAndDraw2DText( + canvas, + text, + selectedItemTopLimit, + selectedItemBottomLimit, + item2CenterOffsetY, + centerToBaselineY + ) + + paint.color = textColor + clipAndDraw2DText( + canvas, + text, + clipTop, + selectedItemTopLimit, + item2CenterOffsetY, + centerToBaselineY + ) + + } else { + // 绘制其他条目 + paint.color = textColor + clipAndDraw2DText( + canvas, + text, + clipTop, + clipBottom, + item2CenterOffsetY, + centerToBaselineY + ) + } + + if (isAutoFitTextSize) { + // 恢复重新测量之前的样式 + paint.textSize = textSize + this.startX = startX + } + } + + /** + * 裁剪并绘制2d text + * + * @param canvas 画布 + * @param text 绘制的文字 + * @param clipTop 裁剪的上边界 + * @param clipBottom 裁剪的下边界 + * @param item2CenterOffsetY 距离中间项的偏移 + * @param centerToBaselineY 文字中心距离baseline的距离 + */ + private fun clipAndDraw2DText( + canvas: Canvas, + text: String, + clipTop: Int, + clipBottom: Int, + item2CenterOffsetY: Int, + centerToBaselineY: Int + ) { + canvas.save() + canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom) + canvas.drawText( + text, + 0, + text.length, + startX.toFloat(), + (this.centerY + item2CenterOffsetY - centerToBaselineY).toFloat(), + paint + ) + canvas.restore() + } + + /** + * 重新测量字体大小 + * + * @param contentText 被测量文字内容 + * @return 文字中心距离baseline的距离 + */ + private fun remeasureTextSize(contentText: String): Int { + var textWidth = paint.measureText(contentText) + var drawWidth = width.toFloat() + var textMargin = textBoundaryMargin * 2 + // 稍微增加了一点文字边距 最大为宽度的1/10 + if (textMargin > drawWidth / 10f) { + drawWidth = drawWidth * 9f / 10f + textMargin = drawWidth / 10f + } else { + drawWidth -= textMargin + } + if (drawWidth <= 0) { + return centerToBaselineY + } + var textSize = this.textSize + while (textWidth > drawWidth) { + textSize-- + if (textSize <= 0) { + break + } + paint.textSize = textSize + textWidth = paint.measureText(contentText) + } + // 重新计算文字起始X + recalculateStartX(textMargin / 2.0f) + // 高度起点也变了 + return recalculateCenterToBaselineY() + } + + /** + * 重新计算字体起始X + * + * @param textMargin 文字外边距 + */ + private fun recalculateStartX(textMargin: Float) { + startX = when (textAlign) { + TEXT_ALIGN_LEFT -> textMargin.toInt() + TEXT_ALIGN_RIGHT -> (width - textMargin).toInt() + else -> width / 2 + } + } + + /** + * 字体大小变化后重新计算距离基线的距离 + * + * @return 文字中心距离baseline的距离 + */ + private fun recalculateCenterToBaselineY(): Int { + val fontMetrics = paint.fontMetrics + // 高度起点也变了 + return (fontMetrics.ascent + (fontMetrics.descent - fontMetrics.ascent) / 2).toInt() + } + + /** + * 绘制弯曲(3D)效果的item + * + * @param canvas 画布 + * @param index 下标 + * @param scrolledOffset 滚动偏移 + */ + private fun draw3DItem(canvas: Canvas, index: Int, scrolledOffset: Int) { + val text = getDataByIndex(index) ?: return + // 滚轮的半径 + val radius = (height - paddingTop - paddingBottom) / 2 + // index 的 item 距离中间项的偏移 + val item2CenterOffsetY = (index - scrollOffsetY / itemHeight) * itemHeight - scrolledOffset + + // 当滑动的角度和y轴垂直时(此时文字已经显示为一条线),不绘制文字 + if (abs(item2CenterOffsetY) > radius * Math.PI / 2) { + return + } + + val angle = item2CenterOffsetY.toDouble() / radius + // 绕x轴滚动的角度 + val rotateX = Math.toDegrees(-angle).toFloat() + // 滚动的距离映射到y轴的长度 + val translateY = (sin(angle) * radius).toFloat() + // 滚动的距离映射到z轴的长度 + val translateZ = ((1 - cos(angle)) * radius).toFloat() + // 透明度 + val alpha = (cos(angle) * 255).toInt() + + // 记录初始测量的字体起始X + val startX = this.startX + // 重新测量字体宽度和基线偏移 + val centerToBaselineY = + if (isAutoFitTextSize) remeasureTextSize(text) else centerToBaselineY + if (abs(item2CenterOffsetY) <= 0) { + // 绘制选中的条目 + paint.color = if (isInactive(index)) textColor else selectedItemTextColor + paint.alpha = 255 + clipAndDraw3DText( + canvas, + text, + selectedItemTopLimit, + selectedItemBottomLimit, + rotateX, + translateY, + translateZ, + centerToBaselineY + ) + } else if (item2CenterOffsetY in 1 until itemHeight) { + // 绘制与下边界交汇的条目 + paint.color = if (isInactive(index)) textColor else selectedItemTextColor + paint.alpha = 255 + clipAndDraw3DText( + canvas, + text, + selectedItemTopLimit, + selectedItemBottomLimit, + rotateX, + translateY, + translateZ, + centerToBaselineY + ) + + paint.color = textColor + paint.alpha = alpha + // 缩小字体,实现折射效果 + val textSize = paint.textSize + paint.textSize = textSize * curvedRefractRatio + // 字体变化,重新计算距离基线偏移 + val reCenterToBaselineY = recalculateCenterToBaselineY() + clipAndDraw3DText( + canvas, + text, + selectedItemBottomLimit, + clipBottom, + rotateX, + translateY, + translateZ, + reCenterToBaselineY + ) + paint.textSize = textSize + } else if (item2CenterOffsetY < 0 && item2CenterOffsetY > -itemHeight) { + // 绘制与上边界交汇的条目 + paint.color = if (isInactive(index)) textColor else selectedItemTextColor + paint.alpha = 255 + clipAndDraw3DText( + canvas, + text, + selectedItemTopLimit, + selectedItemBottomLimit, + rotateX, + translateY, + translateZ, + centerToBaselineY + ) + + paint.color = textColor + paint.alpha = alpha + + // 缩小字体,实现折射效果 + val textSize = paint.textSize + paint.textSize = textSize * curvedRefractRatio + // 字体变化,重新计算距离基线偏移 + val reCenterToBaselineY = recalculateCenterToBaselineY() + clipAndDraw3DText( + canvas, + text, + clipTop, + selectedItemTopLimit, + rotateX, + translateY, + translateZ, + reCenterToBaselineY + ) + paint.textSize = textSize + } else { + // 绘制其他条目 + paint.color = textColor + paint.alpha = alpha + + // 缩小字体,实现折射效果 + val textSize = paint.textSize + paint.textSize = textSize * curvedRefractRatio + // 字体变化,重新计算距离基线偏移 + val reCenterToBaselineY = recalculateCenterToBaselineY() + clipAndDraw3DText( + canvas, + text, + clipTop, + clipBottom, + rotateX, + translateY, + translateZ, + reCenterToBaselineY + ) + paint.textSize = textSize + } + + if (isAutoFitTextSize) { + // 恢复重新测量之前的样式 + paint.textSize = textSize + this.startX = startX + } + } + + /** + * 裁剪并绘制弯曲(3D)效果 + * + * @param canvas 画布 + * @param text 绘制的文字 + * @param clipTop 裁剪的上边界 + * @param clipBottom 裁剪的下边界 + * @param rotateX 绕X轴旋转角度 + * @param offsetY Y轴偏移 + * @param offsetZ Z轴偏移 + * @param centerToBaselineY 文字中心距离baseline的距离 + */ + private fun clipAndDraw3DText( + canvas: Canvas, + text: String, + clipTop: Int, + clipBottom: Int, + rotateX: Float, + offsetY: Float, + offsetZ: Float, + centerToBaselineY: Int + ) { + canvas.save() + canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom) + draw3DText(canvas, text, rotateX, offsetY, offsetZ, centerToBaselineY) + canvas.restore() + } + + /** + * 绘制弯曲(3D)的文字 + * + * @param canvas 画布 + * @param text 绘制的文字 + * @param rotateX 绕X轴旋转角度 + * @param offsetY Y轴偏移 + * @param offsetZ Z轴偏移 + * @param centerToBaselineY 文字中心距离baseline的距离 + */ + private fun draw3DText( + canvas: Canvas, + text: String, + rotateX: Float, + offsetY: Float, + offsetZ: Float, + centerToBaselineY: Int + ) { + camera.save() + camera.translate(0f, 0f, offsetZ) + camera.rotateX(rotateX) + camera.getMatrix(mMatrix) + camera.restore() + + // 调节中心点 + var centerX = this.centerX.toFloat() + // 根据弯曲(3d)对齐方式设置系数 + if (curvedArcDirection == CURVED_ARC_DIRECTION_LEFT) { + centerX = this.centerX * (1 + curvedArcDirectionFactor) + } else if (curvedArcDirection == CURVED_ARC_DIRECTION_RIGHT) { + centerX = this.centerX * (1 - curvedArcDirectionFactor) + } + + val centerY = this.centerY + offsetY + mMatrix.preTranslate(-centerX, -centerY) + mMatrix.postTranslate(centerX, centerY) + + canvas.concat(mMatrix) + canvas.drawText( + text, + 0, + text.length, + startX.toFloat(), + centerY - centerToBaselineY, + paint + ) + + } + + /** + * 根据下标获取到内容 + * + * @param index 下标 + * @return 绘制的文字内容 + */ + private fun getDataByIndex(index: Int): String? { + val dataSize = dataItems.size + if (dataSize == 0) { + return null + } + + var itemText: String? = null + if (isCyclic) { + var i = index % dataSize + if (i < 0) { + i += dataSize + } + itemText = getDataText(dataItems[i]) + } else { + if (index in 0 until dataSize) { + itemText = getDataText(dataItems[index]) + } + } + return itemText + } + + /** + * 获取item text + * + * @param item item数据 + * @return 文本内容 + */ + private fun getDataText(item: Any?): String { + return when (item) { + null -> "" + is Int -> // 如果为整形则最少保留两位数. + if (isIntegerNeedFormat) String.format( + Locale.getDefault(), + integerFormat, + item + ) else item.toString() + + is String -> item + else -> item.toString() + } + } + + /** + * 判断是否不活跃数据 + * + * @param index 数据下标索引 + * @return 是否不活跃数据 + */ + private fun isInactive(index: Int): Boolean { + val dataSize = dataItems.size + if (dataSize == 0) { + return false + } + + if (inactiveIndexes.size == 0) { + return false + } + return if (isCyclic) { + var i = index % dataSize + if (i < 0) { + i += dataSize + } + i in inactiveIndexes + } else { + index in inactiveIndexes + } + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + initVelocityTracker() + velocityTracker!!.addMovement(event) + + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + // 手指按下 + // 处理滑动事件嵌套 拦截事件序列 + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true) + } + // 如果未滚动完成,强制滚动完成 + if (!overScroller.isFinished) { + // 强制滚动完成 + overScroller.forceFinished(true) + isForceFinishScroll = true + } + lastTouchY = event.y + // 按下时间 + downStartTime = System.currentTimeMillis() + } + + MotionEvent.ACTION_MOVE -> { + // 手指移动 + val moveY = event.y + val deltaY = moveY - lastTouchY + + if (onWheelChangedListener != null) { + onWheelChangedListener!!.onWheelScrollStateChanged(SCROLL_STATE_DRAGGING) + } + if (abs(deltaY) >= 1) { + // deltaY 上滑为正,下滑为负 + doScroll((-deltaY).toInt()) + lastTouchY = moveY + invalidateIfYChanged() + } + } + + MotionEvent.ACTION_UP -> { + // 手指抬起 + isForceFinishScroll = false + velocityTracker!!.computeCurrentVelocity(1000, maxFlingVelocity.toFloat()) + val velocityY = velocityTracker!!.yVelocity + if (abs(velocityY) > minFlingVelocity) { + // 快速滑动 + overScroller.forceFinished(true) + isFlingScroll = true + overScroller.fling( + 0, + scrollOffsetY, + 0, + (-velocityY).toInt(), + 0, + 0, + minScrollY, + maxScrollY + ) + } else { + var clickToCenterDistance = 0 + if (System.currentTimeMillis() - downStartTime <= DEFAULT_CLICK_CONFIRM) { + // 处理点击滚动 + // 手指抬起的位置到中心的距离为滚动差值 + clickToCenterDistance = (event.y - centerY).toInt() + } + val scrollRange = + clickToCenterDistance + calculateDistanceToEndPoint((scrollOffsetY + clickToCenterDistance) % itemHeight) + // 平稳滑动 + overScroller.startScroll(0, scrollOffsetY, 0, scrollRange) + } + + invalidateIfYChanged() + ViewCompat.postOnAnimation(this, this) + + // 回收 VelocityTracker + recycleVelocityTracker() + } + + MotionEvent.ACTION_CANCEL -> + // 事件被终止, 回收 + recycleVelocityTracker() + + else -> { + } + } + return true + } + + /** + * 初始化 VelocityTracker + */ + private fun initVelocityTracker() { + if (velocityTracker == null) { + velocityTracker = VelocityTracker.obtain() + } + } + + /** + * 回收 VelocityTracker + */ + private fun recycleVelocityTracker() { + if (velocityTracker != null) { + velocityTracker!!.recycle() + velocityTracker = null + } + } + + /** + * 计算滚动偏移 + * + * @param distance 滚动距离 + */ + private fun doScroll(distance: Int) { + scrollOffsetY += distance + if (!isCyclic) { + // 修正边界 + if (scrollOffsetY < minScrollY) { + scrollOffsetY = minScrollY + } else if (scrollOffsetY > maxScrollY) { + scrollOffsetY = maxScrollY + } + } + } + + /** + * 当Y轴的偏移值改变时再重绘,减少重回次数 + */ + private fun invalidateIfYChanged() { + if (scrollOffsetY != scrolledY) { + scrolledY = scrollOffsetY + // 滚动偏移发生变化 + if (onWheelChangedListener != null) { + onWheelChangedListener!!.onWheelScroll(scrollOffsetY) + } + // 观察item变化 + observeItemChanged() + invalidate() + } + selectedItemPosition = currentScrollPosition + if (onItemSelectedListener != null) { + onItemSelectedListener!!.onItemSelected( + this, + dataItems[currentScrollPosition], + currentScrollPosition + ) + } + } + + /** + * 观察item改变 + */ + private fun observeItemChanged() { + // item改变回调 + val oldPosition = currentScrollPosition + val newPosition = currentPosition + if (oldPosition != newPosition) { + // 改变了 + if (onWheelChangedListener != null) { + onWheelChangedListener!!.onWheelItemChanged(oldPosition, newPosition) + } + // 播放音频 + playSoundEffect() + // 更新下标 + currentScrollPosition = newPosition + } + } + + /** + * 播放滚动音效 + */ + fun playSoundEffect() { + if (isSoundEffect) { + soundHelper.playSoundEffect() + } + } + + /** + * 强制滚动完成,直接停止 + */ + fun forceFinishScroll() { + if (!overScroller.isFinished) { + overScroller.forceFinished(true) + } + } + + /** + * 强制滚动完成,并且直接滚动到最终位置 + */ + fun abortFinishScroll() { + if (!overScroller.isFinished) { + overScroller.abortAnimation() + } + } + + /** + * 计算距离终点的偏移,修正选中条目 + * + * @param remainder 余数 + * @return 偏移量 + */ + private fun calculateDistanceToEndPoint(remainder: Int): Int { + return if (abs(remainder) > this.itemHeight / 2) { + if (scrollOffsetY < 0) { + -this.itemHeight - remainder + } else { + this.itemHeight - remainder + } + } else { + -remainder + } + } + + /** + * 使用run方法而不是computeScroll是因为,invalidate也会执行computeScroll导致回调执行不准确 + */ + override fun run() { + // 停止滚动更新当前下标 + if (overScroller.isFinished && !isForceFinishScroll && !isFlingScroll) { + if (this.itemHeight == 0) { + return + } + // 滚动状态停止 + if (onWheelChangedListener != null) { + onWheelChangedListener!!.onWheelScrollStateChanged(SCROLL_STATE_IDLE) + } + val currentItemPosition = currentPosition + // 当前选中的Position没变时不回调 onItemSelected() + if (currentItemPosition == selectedItemPosition) { + return + } + selectedItemPosition = currentItemPosition + // 停止后重新赋值 + currentScrollPosition = selectedItemPosition + + // 停止滚动,选中条目回调 + if (onItemSelectedListener != null) { + onItemSelectedListener!!.onItemSelected( + this, + dataItems[selectedItemPosition], + selectedItemPosition + ) + } + // 滚动状态回调 + if (onWheelChangedListener != null) { + onWheelChangedListener!!.onWheelSelected(selectedItemPosition) + } + } + + if (overScroller.computeScrollOffset()) { + val oldY = scrollOffsetY + scrollOffsetY = overScroller.currY + + if (oldY != scrollOffsetY) { + if (onWheelChangedListener != null) { + onWheelChangedListener!!.onWheelScrollStateChanged(SCROLL_STATE_SCROLLING) + } + } + invalidateIfYChanged() + ViewCompat.postOnAnimation(this, this) + } else if (isFlingScroll) { + // 滚动完成后,根据是否为快速滚动处理是否需要调整最终位置 + isFlingScroll = false + // 快速滚动后需要调整滚动完成后的最终位置,重新启动scroll滑动到中心位置 + overScroller.startScroll( + 0, + scrollOffsetY, + 0, + calculateDistanceToEndPoint(scrollOffsetY % this.itemHeight) + ) + invalidateIfYChanged() + ViewCompat.postOnAnimation(this, this) + } + } + + + /** + * 根据偏移计算当前位置下标 + * + * @return 偏移量对应的当前下标 + */ + private val currentPosition: Int + get() { + return if (dataItems.size == 0) { + 0 + } else { + val itemPosition = if (scrollOffsetY < 0) { + (scrollOffsetY - this.itemHeight / 2) / this.itemHeight + } else { + (scrollOffsetY + this.itemHeight / 2) / this.itemHeight + } + var currentPosition = itemPosition % dataItems.size + if (currentPosition < 0) { + currentPosition += dataItems.size + } + currentPosition + } + } + + /** + * 获取音效开关状态 + * + * @return 是否开启滚动音效 + */ + fun isSoundEffect(): Boolean { + return isSoundEffect + } + + /** + * 设置音效开关 + * + * @param isSoundEffect 是否开启滚动音效 + */ + fun setSoundEffect(isSoundEffect: Boolean) { + this.isSoundEffect = isSoundEffect + } + + /** + * 设置声音效果资源 + * + * @param rawResId 声音效果资源 越小效果越好 [RawRes] + */ + fun setSoundEffectResource(@RawRes rawResId: Int) { + soundHelper.load(context, rawResId) + } + + /** + * 获取播放音量 + * + * @return 播放音量 range 0.0-1.0 + */ + fun getPlayVolume(): Float { + return soundHelper.playVolume + } + + /** + * 设置播放音量 + * + * @param playVolume 播放音量 range 0.0-1.0 + */ + fun setPlayVolume(@FloatRange(from = 0.0, to = 1.0) playVolume: Float) { + soundHelper.playVolume = playVolume + } + + /** + * 获取指定 position 的数据 + * + * @param position 下标 + * @return position 对应的数据 + */ + fun getItemData(position: Int): Any? { + if (isPositionInRange(position)) { + return dataItems[position] + } else if (dataItems.size in 1..position) { + return dataItems[dataItems.size - 1] + } else if (dataItems.size > 0 && position < 0) { + return dataItems[0] + } + return null + } + + /** + * 获取当前选中的item数据 + * + * @return 当前选中的item数据 + */ + fun getSelectedItemData(): Any? { + return getItemData(selectedItemPosition) + } + + /** + * 获取数据列表 + * + * @return 数据列表 + */ + fun getDataItems(): MutableList { + return dataItems + } + + /** + * 设置数据 + * + * @param dataItems 数据列表 + * @param inactiveIndexes 不活跃的数据下标,选中时不变色;用于做不推荐选中 + */ + @JvmOverloads + fun setDataItems(dataItems: MutableList<*>?, inactiveIndexes: MutableList? = null) { + if (dataItems == null) { + return + } + this.dataItems.clear() + this.dataItems.addAll(dataItems.filterNotNull()) + this.inactiveIndexes.clear() + inactiveIndexes?.let { + this.inactiveIndexes.addAll(it) + } + if (!isResetSelectedPosition && this.dataItems.size > 0) { + // 不重置选中下标 + if (selectedItemPosition >= this.dataItems.size) { + selectedItemPosition = this.dataItems.size - 1 + // 重置滚动下标 + currentScrollPosition = selectedItemPosition + } + } else { + // 重置选中下标和滚动下标 + selectedItemPosition = 0 + currentScrollPosition = selectedItemPosition + } + // 强制滚动完成 + forceFinishScroll() + calculateTextSize() + calculateLimitY() + // 重置滚动偏移 + scrollOffsetY = selectedItemPosition * this.itemHeight + requestLayout() + invalidate() + } + + /** + * 当数据变化时,是否重置选中下标到第一个 + * + * @return 是否重置选中下标到第一个 + */ + fun isResetSelectedPosition(): Boolean { + return isResetSelectedPosition + } + + /** + * 设置当数据变化时,是否重置选中下标到第一个 + * + * @param isResetSelectedPosition 当数据变化时,是否重置选中下标到第一个 + */ + fun setResetSelectedPosition(isResetSelectedPosition: Boolean) { + this.isResetSelectedPosition = isResetSelectedPosition + } + + /** + * 获取字体大小 + * + * @return 字体大小 + */ + fun getTextSize(): Float { + return textSize + } + + /** + * 设置字体大小 + * + * @param textSize 字体大小 + * @param isSp 单位是否是 sp + */ + fun setTextSize(textSize: Float, isSp: Boolean = false) { + val tempTextSize = textSize + this.textSize = if (isSp) sp2px(textSize) else textSize + if (tempTextSize == this.textSize) { + return + } + // 强制滚动完成 + forceFinishScroll() + calculateTextSize() + calculateDrawStart() + calculateLimitY() + // 字体大小变化,偏移距离也变化了 + scrollOffsetY = selectedItemPosition * this.itemHeight + requestLayout() + invalidate() + } + + /** + * 获取是否自动调整字体大小,以显示完全 + * + * @return 是否自动调整字体大小 + */ + fun isAutoFitTextSize(): Boolean { + return isAutoFitTextSize + } + + /** + * 设置是否自动调整字体大小,以显示完全 + * + * @param isAutoFitTextSize 是否自动调整字体大小 + */ + fun setAutoFitTextSize(isAutoFitTextSize: Boolean) { + this.isAutoFitTextSize = isAutoFitTextSize + invalidate() + } + + /** + * 获取当前字体 + * + * @return 字体 + */ + fun getTypeface(): Typeface { + return paint.typeface + } + + /** + * 设置当前字体 + * + * @param typeface 字体 + */ + fun setTypeface(typeface: Typeface) { + if (paint.typeface === typeface) { + return + } + // 强制滚动完成 + forceFinishScroll() + paint.typeface = typeface + calculateTextSize() + calculateDrawStart() + // 字体大小变化,偏移距离也变化了 + scrollOffsetY = selectedItemPosition * itemHeight + calculateLimitY() + requestLayout() + invalidate() + } + + /** + * 获取item间距 + * + * @return 行间距值 + */ + fun getLineSpacing(): Float { + return lineSpacing + } + + /** + * 设置item间距 + * + * @param lineSpacing 行间距值 + * @param isDp lineSpacing 单位是否为 dp + */ + fun setLineSpacing(lineSpacing: Float, isDp: Boolean = false) { + val tempLineSpace = lineSpacing + this.lineSpacing = if (isDp) dp2px(lineSpacing) else lineSpacing + if (tempLineSpace == lineSpacing) { + return + } + scrollOffsetY = 0 + calculateTextSize() + requestLayout() + invalidate() + } + + /** + * 获取数据为Integer类型时是否需要转换 + * + * @return isIntegerNeedFormat + */ + fun isIntegerNeedFormat(): Boolean { + return isIntegerNeedFormat + } + + /** + * 设置数据为Integer类型时是否需要转换 + * + * @param isIntegerNeedFormat 数据为Integer类型时是否需要转换 + */ + fun setIntegerNeedFormat(isIntegerNeedFormat: Boolean) { + if (this.isIntegerNeedFormat == isIntegerNeedFormat) { + return + } + this.isIntegerNeedFormat = isIntegerNeedFormat + calculateTextSize() + requestLayout() + invalidate() + } + + /** + * 同时设置 isIntegerNeedFormat=true 和 mIntegerFormat=integerFormat + * + * @param integerFormat + */ + fun setIntegerNeedFormat(integerFormat: String) { + isIntegerNeedFormat = true + this.integerFormat = integerFormat + calculateTextSize() + requestLayout() + invalidate() + } + + /** + * 跳转可见条目数为奇数 + * + * @param visibleItems 可见条目数 + * @return 调整后的可见条目数 + */ + private fun adjustVisibleItems(visibleItems: Int): Int { + return abs(visibleItems / 2 * 2 + 1) // 当传入的值为偶数时,换算成奇数; + } + + /** + * 是否是循环滚动 + * + * @return 是否是循环滚动 + */ + fun isCyclic(): Boolean { + return isCyclic + } + + /** + * 设置是否循环滚动 + * + * @param isCyclic 是否是循环滚动 + */ + fun setCyclic(isCyclic: Boolean) { + if (this.isCyclic == isCyclic) { + return + } + this.isCyclic = isCyclic + + forceFinishScroll() + calculateLimitY() + // 设置当前选中的偏移值 + scrollOffsetY = selectedItemPosition * itemHeight + invalidate() + } + + /** + * 设置当前选中下标 + * + * @param position 下标 + * @param isSmoothScroll 是否平滑滚动 + */ + fun setSelectedItemPosition(position: Int, isSmoothScroll: Boolean = false) { + this.selectItemPosition = position + this.isSmoothScroll = isSmoothScroll + setSelectedItemPosition(position, isSmoothScroll, 0) + } + + /** + * 设置当前选中下标 + * + * @param position 下标 + * @param isSmoothScroll 是否平滑滚动 + * @param smoothDuration 平滑滚动时间 + */ + fun setSelectedItemPosition(position: Int, isSmoothScroll: Boolean, smoothDuration: Int) { + if (!isPositionInRange(position)) { + return + } + + // item之间差值 + val itemDistance = position * itemHeight - scrollOffsetY + // 如果Scroller滑动未停止,强制结束动画 + abortFinishScroll() + + if (isSmoothScroll) { + // 如果是平滑滚动并且之前的Scroll滚动完成 + overScroller.startScroll( + 0, + scrollOffsetY, + 0, + itemDistance, + if (smoothDuration > 0) smoothDuration else DEFAULT_SCROLL_DURATION + ) + invalidateIfYChanged() + ViewCompat.postOnAnimation(this, this) + + } else { + doScroll(itemDistance) + selectedItemPosition = position + // 选中条目回调 + if (onItemSelectedListener != null) { + onItemSelectedListener!!.onItemSelected( + this, + this.dataItems[selectedItemPosition], + selectedItemPosition + ) + } + + if (onWheelChangedListener != null) { + onWheelChangedListener!!.onWheelSelected(selectedItemPosition) + } + invalidateIfYChanged() + } + + } + + /** + * 判断下标是否在数据列表范围内 + * + * @param position 下标 + * @return 是否在数据列表范围内 + */ + fun isPositionInRange(position: Int): Boolean { + return position >= 0 && position < this.dataItems.size + } + + /** + * 获取是否显示分割线 + * + * @return 是否显示分割线 + */ + fun isShowDivider(): Boolean { + return isShowDivider + } + + /** + * 设置是否显示分割线 + * + * @param isShowDivider 是否显示分割线 + */ + fun setShowDivider(isShowDivider: Boolean) { + if (this.isShowDivider == isShowDivider) { + return + } + this.isShowDivider = isShowDivider + invalidate() + } + + /** + * 获取是否绘制选中区域 + * + * @return 是否绘制选中区域 + */ + fun isDrawSelectedRect(): Boolean { + return isDrawSelectedRect + } + + /** + * 设置是否绘制选中区域 + * + * @param isDrawSelectedRect 是否绘制选中区域 + */ + fun setDrawSelectedRect(isDrawSelectedRect: Boolean) { + this.isDrawSelectedRect = isDrawSelectedRect + invalidate() + } + + /** + * 获取选中区域颜色 + * + * @return 选中区域颜色 ColorInt + */ + fun getSelectedRectColor(): Int { + return selectedRectColor + } + + /** + * 设置选中区域颜色 + * + * @param selectedRectColor 选中区域颜色 [ColorInt] + */ + fun setSelectedRectColor(@ColorInt selectedRectColor: Int) { + this.selectedRectColor = selectedRectColor + invalidate() + } + + /** + * 设置选中区域颜色 + * + * @param selectedRectColorRes 选中区域颜色 [ColorRes] + */ + fun setSelectedRectColorRes(@ColorRes selectedRectColorRes: Int) { + setSelectedRectColor(ContextCompat.getColor(context, selectedRectColorRes)) + } + + /** + * 设置选中区域圆角 + * + * @param selectedRectRadius 选中区域圆角 + */ + fun setSelectedRectRadius(selectedRectRadius: Float) { + this.selectedRectLeftRadius = selectedRectRadius + this.selectedRectRightRadius = selectedRectRadius + invalidate() + } + + /** + * 获取选中区域左侧圆角 + */ + fun getSelectedRectLeftRadius(): Float { + return selectedRectLeftRadius + } + + /** + * 设置选中区域左侧圆角 + * + * @param selectedLeftRectRadius 选中区域左侧圆角 + */ + fun setSelectedRectLeftRadius(selectedLeftRectRadius: Float) { + this.selectedRectLeftRadius = selectedLeftRectRadius + invalidate() + } + + /** + * 获取选中区域右侧圆角 + */ + fun getSelectedRectRightRadius(): Float { + return selectedRectRightRadius + } + + /** + * 设置选中区域右侧圆角 + * + * @param selectedRightRectRadius 选中区域右侧圆角 + */ + fun setSelectedRectRightRadius(selectedRightRectRadius: Float) { + this.selectedRectRightRadius = selectedRightRectRadius + invalidate() + } + + /** + * 获取是否是弯曲(3D)效果 + * + * @return 是否是弯曲(3D)效果 + */ + fun isCurved(): Boolean { + return isCurved + } + + /** + * 设置是否是弯曲(3D)效果 + * + * @param isCurved 是否是弯曲(3D)效果 + */ + fun setCurved(isCurved: Boolean) { + if (this.isCurved == isCurved) { + return + } + this.isCurved = isCurved + calculateTextSize() + requestLayout() + invalidate() + } + + /** + * 获取弯曲(3D)效果左右圆弧效果方向 + * + * @return 左右圆弧效果方向 [.CURVED_ARC_DIRECTION_LEFT] + * [.CURVED_ARC_DIRECTION_CENTER] + * [.CURVED_ARC_DIRECTION_RIGHT] + */ + fun getCurvedArcDirection(): Int { + return curvedArcDirection + } + + /** + * 设置弯曲(3D)效果左右圆弧效果方向 + * + * @param curvedArcDirection 左右圆弧效果方向 [.CURVED_ARC_DIRECTION_LEFT] + * [.CURVED_ARC_DIRECTION_CENTER] + * [.CURVED_ARC_DIRECTION_RIGHT] + */ + fun setCurvedArcDirection(@CurvedArcDirection curvedArcDirection: Int) { + if (this.curvedArcDirection == curvedArcDirection) { + return + } + this.curvedArcDirection = curvedArcDirection + invalidate() + } + + /** + * 获取弯曲(3D)效果左右圆弧偏移效果方向系数 + * + * @return 左右圆弧偏移效果方向系数 + */ + fun getCurvedArcDirectionFactor(): Float { + return curvedArcDirectionFactor + } + + /** + * 设置弯曲(3D)效果左右圆弧偏移效果方向系数 + * + * @param curvedArcDirectionFactor 左右圆弧偏移效果方向系数 range 0.0-1.0 越大越明显 + */ + fun setCurvedArcDirectionFactor( + @FloatRange( + from = 0.0, + to = 1.0 + ) curvedArcDirectionFactor: Float + ) { + var tempcurvedArcDirectionFactor = curvedArcDirectionFactor + if (this.curvedArcDirectionFactor == tempcurvedArcDirectionFactor) { + return + } + if (curvedArcDirectionFactor < 0) { + tempcurvedArcDirectionFactor = 0f + } else if (curvedArcDirectionFactor > 1) { + tempcurvedArcDirectionFactor = 1f + } + this.curvedArcDirectionFactor = tempcurvedArcDirectionFactor + invalidate() + } + + /** + * 获取折射偏移比例 + * + * @return 折射偏移比例 + */ + fun getCurvedRefractRatio(): Float { + return curvedRefractRatio + } + + /** + * 设置选中条目折射偏移比例 + * + * @param curvedRefractRatio 折射偏移比例 range 0.0-1.0 + */ + fun setCurvedRefractRatio(@FloatRange(from = 0.0, to = 1.0) curvedRefractRatio: Float) { + val tempRefractRatio = this.curvedRefractRatio + this.curvedRefractRatio = curvedRefractRatio + if (this.curvedRefractRatio > 1f) { + this.curvedRefractRatio = 1.0f + } else if (this.curvedRefractRatio < 0f) { + this.curvedRefractRatio = + DEFAULT_REFRACT_RATIO + } + if (tempRefractRatio == this.curvedRefractRatio) { + return + } + invalidate() + } + + /** + * 获取选中监听 + * + * @return 选中监听器 + */ + fun getOnItemSelectedListener(): OnItemSelectedListener? { + return onItemSelectedListener + } + + /** + * 设置选中监听 + * + * @param onItemSelectedListener 选中监听器 + */ + fun setOnItemSelectedListener(onItemSelectedListener: OnItemSelectedListener) { + this.onItemSelectedListener = onItemSelectedListener + setSelectedItemPosition(selectedItemPosition, isSmoothScroll, 0) + } + + /** + * 获取滚动变化监听 + * + * @return 滚动变化监听器 + */ + fun getOnWheelChangedListener(): OnWheelChangedListener? { + return onWheelChangedListener + } + + /** + * 设置滚动变化监听 + * + * @param onWheelChangedListener 滚动变化监听器 + */ + fun setOnWheelChangedListener(onWheelChangedListener: OnWheelChangedListener) { + this.onWheelChangedListener = onWheelChangedListener + } + + /** + * 条目选中监听器 + */ + interface OnItemSelectedListener { + + /** + * 条目选中回调 + * + * @param wheelView wheelView + * @param data 选中的数据 + * @param position 选中的下标 + */ + fun onItemSelected(wheelView: WheelView, data: Any, position: Int) + } + + /** + * WheelView滚动状态改变监听器 + */ + interface OnWheelChangedListener { + + /** + * WheelView 滚动 + * + * @param scrollOffsetY 滚动偏移 + */ + fun onWheelScroll(scrollOffsetY: Int) + + /** + * WheelView 条目变化 + * + * @param oldPosition 旧的下标 + * @param newPosition 新下标 + */ + fun onWheelItemChanged(oldPosition: Int, newPosition: Int) + + /** + * WheelView 选中 + * + * @param position 选中的下标 + */ + fun onWheelSelected(position: Int) + + /** + * WheelView 滚动状态 + * + * @param state 滚动状态 [WheelView.SCROLL_STATE_IDLE] + * [WheelView.SCROLL_STATE_DRAGGING] + * [WheelView.SCROLL_STATE_SCROLLING] + */ + fun onWheelScrollStateChanged(state: Int) + } + + open class SimpleOnWheelChangedListener : OnWheelChangedListener { + override fun onWheelScroll(scrollOffsetY: Int) {} + + override fun onWheelItemChanged(oldPosition: Int, newPosition: Int) {} + + override fun onWheelSelected(position: Int) {} + + override fun onWheelScrollStateChanged(state: Int) {} + } + + /** + * SoundPool 辅助类 + */ + private class SoundHelper { + + private val soundPool: SoundPool = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + SoundPool.Builder().build() + } else { + @Suppress("DEPRECATION") + SoundPool(1, AudioManager.STREAM_SYSTEM, 1) + } + + private var soundId: Int = 0 + + /** + * 音频播放音量 range 0.0-1.0 + */ + var playVolume: Float = 0.toFloat() + + /** + * 加载音频资源 + * + * @param context 上下文 + * @param resId 音频资源 [RawRes] + */ + fun load(context: Context, @RawRes resId: Int) { + soundId = soundPool.load(context, resId, 1) + } + + /** + * 播放声音效果 + */ + fun playSoundEffect() { + if (soundId != 0) { + soundPool.play(soundId, playVolume, playVolume, 1, 0, 1f) + } + } + + /** + * 释放SoundPool + */ + fun release() { + soundPool.release() + } + } + + companion object { + + private val DEFAULT_LINE_SPACING = + dp2px(2f) + + private val DEFAULT_TEXT_SIZE = + sp2px(15f) + + private val DEFAULT_TEXT_BOUNDARY_MARGIN = + dp2px(2f) + + private val DEFAULT_DIVIDER_HEIGHT = + dp2px(1f) + + private const val DEFAULT_NORMAL_TEXT_COLOR = Color.DKGRAY + + private const val DEFAULT_SELECTED_TEXT_COLOR = Color.BLACK + + private const val DEFAULT_VISIBLE_ITEM = 5 + + private const val DEFAULT_SCROLL_DURATION = 250 + + private const val DEFAULT_CLICK_CONFIRM: Long = 120 + + private const val DEFAULT_INTEGER_FORMAT = "%02d" + + /** + * 默认折射比值,通过字体大小来实现折射视觉差 + */ + private const val DEFAULT_REFRACT_RATIO = 0.9f + + /** + * 文字对齐方式 + */ + const val TEXT_ALIGN_LEFT = 0 + + const val TEXT_ALIGN_CENTER = 1 + + const val TEXT_ALIGN_RIGHT = 2 + + /** + * 滚动状态 + */ + const val SCROLL_STATE_IDLE = 0 + + const val SCROLL_STATE_DRAGGING = 1 + + const val SCROLL_STATE_SCROLLING = 2 + + /** + * 弯曲效果对齐方式 + */ + const val CURVED_ARC_DIRECTION_LEFT = 0 + + const val CURVED_ARC_DIRECTION_CENTER = 1 + + const val CURVED_ARC_DIRECTION_RIGHT = 2 + + const val DEFAULT_CURVED_FACTOR = 0.75f + + /** + * 分割线填充类型 + */ + const val DIVIDER_TYPE_FILL = 0 + + const val DIVIDER_TYPE_WRAP = 1 + + /** + * dp转换px + * + * @param dp dp值 + * @return 转换后的px值 + */ + private fun dp2px(dp: Float): Float { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp, + Resources.getSystem().displayMetrics + ) + } + + /** + * sp转换px + * + * @param sp sp值 + * @return 转换后的px值 + */ + private fun sp2px(sp: Float): Float { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + sp, + Resources.getSystem().displayMetrics + ) + } + } +} + + diff --git a/baselib/src/main/res/values/attrs.xml b/baselib/src/main/res/values/attrs.xml index fc3677f..8577308 100755 --- a/baselib/src/main/res/values/attrs.xml +++ b/baselib/src/main/res/values/attrs.xml @@ -227,4 +227,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a0d9dbc..06e95f2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,6 @@ mmkv = "1.3.9" rxjava = "3.0.0" lifecycleViewModel = "2.8.5" glide = "4.16.0" -permissionX = "1.8.1" lifecycleRuntimeKtx = "2.6.1" activityCompose = "1.8.0" composeBom = "2024.04.01" @@ -34,8 +33,8 @@ lifecycleLivedataKtx = "2.6.1" fragmentKtx = "1.5.6" smartRefreshLayout = "2.1.0" jjwt = "3.10.3" - - +emoji2Emojipicker = "1.0.0-alpha03" +xxpermissions = "20.0" @@ -56,7 +55,6 @@ rxjava = { group = "io.reactivex.rxjava3", name = "rxjava", version.ref = "rxjav rxkotlin = { group = "io.reactivex.rxjava3", name = "rxkotlin", version.ref = "rxjava" } lifecycleViewModel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewModel" } glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide" } -permissionX = { group = "com.guolindev.permissionx", name = "permissionx", version.ref = "permissionX" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } @@ -83,10 +81,9 @@ refreshHeaderCircle = { group = "io.github.scwang90", name = "refresh-header-cla refreshFoot = { group = "io.github.scwang90", name = "refresh-footer-classics", version.ref = "smartRefreshLayout" } refreshFalsify = { group = "io.github.scwang90", name = "refresh-header-falsify", version.ref = "smartRefreshLayout" } refreshMaterial = { group = "io.github.scwang90", name = "refresh-header-material", version.ref = "smartRefreshLayout" } -#implementation 'io.github.scwang90:refresh-header-falsify:2.1.0' //虚拟刷新头 -#implementation 'io.github.scwang90:refresh-header-material:2.1.0' //谷歌刷新头 jjwt = { group = "com.auth0", name = "java-jwt", version.ref = "jjwt" } - +androidx-emoji2-emojipicker = { group = "androidx.emoji2", name = "emoji2-emojipicker", version.ref = "emoji2Emojipicker" } +xxpermissions = { group = "com.github.getActivity", name = "XXPermissions", version.ref = "xxpermissions" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 967b82a..f7cd185 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,6 +16,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven(url = "https://jitpack.io") } }