diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatBubble.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatBubble.kt index a13570b..a3e3028 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatBubble.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatBubble.kt @@ -6,11 +6,6 @@ import kotlinx.parcelize.Parcelize @Parcelize data class ChatBubble( - /** - * code - */ - val code: String, - /** * id */ @@ -40,7 +35,7 @@ data class ChatBubble( * 解锁类型 MEMBER:会员 HEARTBEAT_LEVEL:心动等级 */ val unlockType: String? = null, - var isDefault: Boolean, + var isDefault: Boolean = false, var select: Boolean = false ) : Parcelable { companion object { diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatMode.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatMode.kt new file mode 100644 index 0000000..41ad93b --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatMode.kt @@ -0,0 +1,22 @@ +package com.remax.visualnovel.entity.response + +/** + * Created by HJW on 2025/8/18 + */ +data class ChatMode( + + /** + * 对话模型描述 + */ + val description: String? = null, + + /** + * 对话模型名称 + */ + val name: String? = null, + + + val onlyVip: Boolean = false, + + var isSelected: Boolean = false, +) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/ChatSettingView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/ChatSettingView.kt index e74a3a2..1fc2bab 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/ChatSettingView.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/ChatSettingView.kt @@ -9,6 +9,8 @@ import android.widget.LinearLayout import androidx.core.graphics.toColorInt import com.remax.visualnovel.R import com.remax.visualnovel.databinding.LayoutChatMenuViewBinding +import com.remax.visualnovel.entity.response.ChatBubble +import com.remax.visualnovel.entity.response.ChatMode import com.remax.visualnovel.entity.response.ChatSound import com.remax.visualnovel.ui.chat.ui.expandableSelector.SelectorItem @@ -27,7 +29,9 @@ class ChatSettingView @JvmOverloads constructor( } initAiModelSelectorView() + initChatModeSelectorView() initSoundSelectorView() + initBubbleSelectView() } @@ -67,6 +71,34 @@ class ChatSettingView @JvmOverloads constructor( mBinding.aiModelSelector.selectItem(0) } + fun initChatModeSelectorView() { + val items = listOf( + ChatMode( + name = "Mode-1", + description = "Previous-generation large model", + ), + ChatMode( + name = "Mode-2", + description = "aaaaaaaaaaaaaaaaaaa", + ), + ChatMode( + name = "Mode-3", + description = "ccccccccccccccccccccccccc", + ), + ChatMode( + name = "Mode-4", + description = "Pppppppppppppppppppppp", + ) + ) + + //aiModelSelector.setOnItemSelectedListener() + mBinding.chatModelSelector.setTitleIcon(R.mipmap.setting_chat_mode_icon) + mBinding.chatModelSelector.setTitleText(R.string.chat_mode) + mBinding.chatModelSelector.setItems(items) + mBinding.chatModelSelector.selectItem(0) + } + + fun initSoundSelectorView() { val items = listOf( ChatSound( @@ -74,14 +106,14 @@ class ChatSettingView @JvmOverloads constructor( name = "Sound-1", description = "This is description for sound-1", isMale = true, - imgUrl = "aa" + imgUrl = "" ), ChatSound( id = 2L, name = "Sound-2", description = "This is description for sound-2", isMale = true, - imgUrl = "aa" + imgUrl = "" ), ChatSound( @@ -89,7 +121,7 @@ class ChatSettingView @JvmOverloads constructor( name = "Sound-3", description = "This is description for sound-3", isMale = true, - imgUrl = "aa" + imgUrl = "" ), ChatSound( @@ -97,7 +129,7 @@ class ChatSettingView @JvmOverloads constructor( name = "Sound-4", description = "This is description for sound-4", isMale = true, - imgUrl = "aa" + imgUrl = "" ), ChatSound( @@ -105,12 +137,47 @@ class ChatSettingView @JvmOverloads constructor( name = "Sound-5", description = "This is description for sound-5", isMale = true, - imgUrl = "aa" + imgUrl = "" ) ) mBinding.soundActorSelector.setItems(items) } + fun initBubbleSelectView() { + val items = listOf( + ChatBubble( + id = 1L, + name = "Bubble-1", + imgUrl = "" + ), + ChatBubble( + id = 2L, + name = "Bubble-2", + imgUrl = "" + ), + + ChatBubble( + id = 3L, + name = "Bubble-3", + imgUrl = "" + ), + + ChatBubble( + id = 4L, + name = "Bubble-4", + imgUrl = "" + ), + + ChatBubble( + id = 5L, + name = "Bubble-5", + imgUrl = "" + ) + ) + + mBinding.bubbleSelectView.setItems(items) + } + } diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/expandableSelector/ExpandBubbleSelectView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/expandableSelector/ExpandBubbleSelectView.kt new file mode 100644 index 0000000..9fe2684 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/expandableSelector/ExpandBubbleSelectView.kt @@ -0,0 +1,138 @@ +package com.remax.visualnovel.ui.chat.ui.expandableSelector + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ObjectAnimator +import android.animation.ValueAnimator +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.animation.AccelerateDecelerateInterpolator +import android.widget.LinearLayout +import com.remax.visualnovel.R +import com.remax.visualnovel.databinding.LayoutExpandSelectViewBinding +import com.remax.visualnovel.entity.response.ChatBubble +import com.remax.visualnovel.entity.response.ChatSound + + +class ExpandBubbleSelectView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + private lateinit var mBinding: LayoutExpandSelectViewBinding + private lateinit var mExpandSubView : ExpandBubbleSubView + + private var isExpanded = false + private var animationDuration = 300 + private var itemSelectedListener: OnItemSelectedListener? = null + + init { + initView(context, attrs) + } + + private fun initView(context: Context, attrs: AttributeSet?) { + mBinding = LayoutExpandSelectViewBinding.inflate(LayoutInflater.from(context), this, true) + + mExpandSubView = ExpandBubbleSubView(context) + mBinding.itemsContainer.addView(mExpandSubView, + LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + ) + setTitleText(R.string.setting_chat_bubble_title) + setTitleIcon(R.mipmap.setting_bubble_icon) + setupClickListeners() + } + + private fun setupClickListeners() { + mBinding.titleLayout.setOnClickListener { toggle() } + } + + + fun setTitleIcon(resId: Int) { + mBinding.icon.setImageResource(resId) + } + + fun setTitleText(titleRes: Int) { + mBinding.titleText.text = context.resources.getString(titleRes) + } + + fun setItems(newItems: List) { + mExpandSubView.setItems(newItems) + } + + + fun toggle() { + if (isExpanded) collapse() else expand() + } + + fun expand() { + if (isExpanded) return + + isExpanded = true + mBinding.itemsContainer.visibility = VISIBLE + animateArrow(0f, 90f) + // param height anim + val animator = ValueAnimator.ofInt(0, getItemsHeight()) + animator.duration = animationDuration.toLong() + animator.interpolator = AccelerateDecelerateInterpolator() + animator.addUpdateListener { animation -> + val value = animation.animatedValue as Int + val params = mBinding.itemsContainer.layoutParams + params.height = value + mBinding.itemsContainer.layoutParams = params + } + animator.start() + } + + + fun collapse() { + if (!isExpanded) return + + isExpanded = false + animateArrow(90f, 0f) + // param height anim + val animator = ValueAnimator.ofInt(getItemsHeight(), 0) + animator.duration = animationDuration.toLong() + animator.interpolator = AccelerateDecelerateInterpolator() + animator.addUpdateListener { animation -> + val value = animation.animatedValue as Int + val params = mBinding.itemsContainer.layoutParams + params.height = value + mBinding.itemsContainer.layoutParams = params + } + animator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + mBinding.itemsContainer.visibility = GONE + } + }) + animator.start() + } + + private fun animateArrow(from: Float, to: Float) { + val rotation = ObjectAnimator.ofFloat(mBinding.arrow, "rotation", from, to) + rotation.duration = animationDuration.toLong() + rotation.interpolator = AccelerateDecelerateInterpolator() + rotation.start() + } + + private fun getItemsHeight(): Int { + var height = 0 + for (i in 0 until mBinding.itemsContainer.childCount) { + val child = mBinding.itemsContainer.getChildAt(i) + child.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) + ) + height += child.measuredHeight + } + return height + } + + fun setOnItemSelectedListener(listener: OnItemSelectedListener) { + this.itemSelectedListener = listener + } + + interface OnItemSelectedListener { + fun onItemSelected(position: Int, item: SelectorItem) + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/expandableSelector/ExpandBubbleSubView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/expandableSelector/ExpandBubbleSubView.kt index b488795..4664683 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/expandableSelector/ExpandBubbleSubView.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/expandableSelector/ExpandBubbleSubView.kt @@ -6,36 +6,74 @@ import android.view.LayoutInflater import android.widget.LinearLayout import androidx.recyclerview.widget.RecyclerView import com.drake.brv.annotaion.DividerOrientation +import com.drake.brv.utils.bindingAdapter import com.drake.brv.utils.divider import com.drake.brv.utils.grid +import com.drake.brv.utils.models import com.drake.brv.utils.setup +import com.remax.visualnovel.R +import com.remax.visualnovel.databinding.LayoutItemSettingBubbleBinding import com.remax.visualnovel.databinding.LayoutSettingBubbleSubViewBinding +import com.remax.visualnovel.entity.response.ChatBubble +import com.remax.visualnovel.extension.glide.load + class ExpandBubbleSubView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : LinearLayout(context, attrs, defStyleAttr) { - + private lateinit var items: List private var mBinding: LayoutSettingBubbleSubViewBinding init { - mBinding = LayoutSettingBubbleSubViewBinding.inflate(LayoutInflater.from(context)) + mBinding = LayoutSettingBubbleSubViewBinding.inflate(LayoutInflater.from(context), this, true) with(mBinding) { initRv(itemsRv) } } + fun setSelectedSound() { + + } + private fun initRv(itemsRv: RecyclerView) { - itemsRv.grid(2) + itemsRv.grid(3) .divider { setDivider(16, true) orientation = DividerOrientation.VERTICAL }.setup { + addType(R.layout.layout_item_setting_bubble) + onClick(R.id.tv_select) { + val bubble = getModel() + if (!bubble.select) { + itemsRv.bindingAdapter.models?.filterIsInstance()?.forEach { item -> + item.select = item == bubble + } + itemsRv.bindingAdapter.notifyDataSetChanged() + setSelectedSound() + } + } + + onBind { + val item = getModel() + with(getBinding()) { + if (!item.imgUrl.isNullOrEmpty()) { + ivBubble.load(item.imgUrl) + } + + ivBubbleName.text = item.name + } + } } } + fun setItems(newItems: List) { + items = newItems + mBinding.itemsRv.models = items + } + } \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/expandableSelector/ExpandChatModeSelectView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/expandableSelector/ExpandChatModeSelectView.kt new file mode 100644 index 0000000..553af75 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/expandableSelector/ExpandChatModeSelectView.kt @@ -0,0 +1,201 @@ +package com.remax.visualnovel.ui.chat.ui.expandableSelector + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ObjectAnimator +import android.animation.ValueAnimator +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator +import android.widget.LinearLayout +import com.remax.visualnovel.R +import com.remax.visualnovel.databinding.LayoutExpandSelectViewBinding +import com.remax.visualnovel.databinding.LayoutItemChatModeBinding +import com.remax.visualnovel.entity.response.ChatMode +import com.remax.visualnovel.utils.spannablex.utils.dp + + +class ExpandChatModeSelectView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + private lateinit var mBinding: LayoutExpandSelectViewBinding + + private var isExpanded = false + private var animationDuration = 300 + private var items: List = emptyList() + private var itemSelectedListener: OnItemSelectedListener? = null + + init { + initView(context, attrs) + } + + private fun initView(context: Context, attrs: AttributeSet?) { + mBinding = LayoutExpandSelectViewBinding.inflate(LayoutInflater.from(context), this, true) + mBinding.itemsContainer.setBackgroundResource(R.drawable.bg_expand_view_items) + setupAttributes(attrs) + setupClickListeners() + } + + private fun setupAttributes(attrs: AttributeSet?) { + attrs?.let { + val typedArray = context.obtainStyledAttributes(it, R.styleable.ExpandableSelector) + val title = typedArray.getString(R.styleable.ExpandableSelector_titleText) + title?.let { mBinding.titleText.text = it } + animationDuration = typedArray.getInt(R.styleable.ExpandableSelector_animationDuration, 300) + typedArray.recycle() + } + } + + private fun setupClickListeners() { + mBinding.titleLayout.setOnClickListener { toggle() } + } + + + fun setTitleIcon(resId: Int) { + mBinding.icon.setImageResource(resId) + } + + fun setTitleText(titleRes: Int) { + mBinding.titleText.text = context.resources.getString(titleRes) + } + + fun setItems(newItems: List) { + items = newItems + updateItemsView() + } + + + + private fun updateItemsView() { + mBinding.itemsContainer.removeAllViews() + + items.forEachIndexed { index, item -> + val itemView = createChatModeItemView(item, index) + mBinding.itemsContainer.addView(itemView) + + if (index < items.size - 1) { + addDivider() + } + } + } + + + private fun createChatModeItemView(item: ChatMode, position: Int): View { + val binding = LayoutItemChatModeBinding.inflate(LayoutInflater.from(context), mBinding.itemsContainer, false) + binding.itemName.text = item.name + binding.modeDescription.text = item.description + + + binding.root.setOnClickListener { + selectItem(position) + itemSelectedListener?.onItemSelected(position, item) + } + return binding.root + } + + private fun addDivider() { + val divider = View(context).apply { + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, 1.dp).apply { + setBackgroundColor(context.resources.getColor(R.color.chat_setting_divider_color)) + marginStart = 10.dp + marginEnd = 10.dp + } + } + mBinding.itemsContainer.addView(divider) + } + + + fun selectItem(position: Int) { + items.forEachIndexed { index, item -> + item.isSelected = index == position + } + updateItemsView() + + if (position in items.indices) { + mBinding.titleText.text = items[position].name + } + + collapse() + } + + + fun toggle() { + if (isExpanded) collapse() else expand() + } + + fun expand() { + if (isExpanded) return + + isExpanded = true + mBinding.itemsContainer.visibility = VISIBLE + animateArrow(0f, 90f) + // param height anim + val animator = ValueAnimator.ofInt(0, getItemsHeight()) + animator.duration = animationDuration.toLong() + animator.interpolator = AccelerateDecelerateInterpolator() + animator.addUpdateListener { animation -> + val value = animation.animatedValue as Int + val params = mBinding.itemsContainer.layoutParams + params.height = value + mBinding.itemsContainer.layoutParams = params + } + animator.start() + } + + + fun collapse() { + if (!isExpanded) return + + isExpanded = false + animateArrow(90f, 0f) + // param height anim + val animator = ValueAnimator.ofInt(getItemsHeight(), 0) + animator.duration = animationDuration.toLong() + animator.interpolator = AccelerateDecelerateInterpolator() + animator.addUpdateListener { animation -> + val value = animation.animatedValue as Int + val params = mBinding.itemsContainer.layoutParams + params.height = value + mBinding.itemsContainer.layoutParams = params + } + animator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + mBinding.itemsContainer.visibility = GONE + } + }) + animator.start() + } + + private fun animateArrow(from: Float, to: Float) { + val rotation = ObjectAnimator.ofFloat(mBinding.arrow, "rotation", from, to) + rotation.duration = animationDuration.toLong() + rotation.interpolator = AccelerateDecelerateInterpolator() + rotation.start() + } + + private fun getItemsHeight(): Int { + var height = 0 + for (i in 0 until mBinding.itemsContainer.childCount) { + val child = mBinding.itemsContainer.getChildAt(i) + child.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) + ) + height += child.measuredHeight + } + return height + } + + fun setOnItemSelectedListener(listener: OnItemSelectedListener) { + this.itemSelectedListener = listener + } + + interface OnItemSelectedListener { + fun onItemSelected(position: Int, item: ChatMode) + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/expandableSelector/ExpandSelectView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/expandableSelector/ExpandSelectView.kt index 64998fc..e60857f 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/expandableSelector/ExpandSelectView.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ui/expandableSelector/ExpandSelectView.kt @@ -86,7 +86,7 @@ class ExpandSelectView @JvmOverloads constructor( private fun createItemView(item: SelectorItem, position: Int): View { val itemView = LayoutInflater.from(context) - .inflate(R.layout.layout_expand_view_item, mBinding.itemsContainer, false) + .inflate(R.layout.layout_item_ai_model, mBinding.itemsContainer, false) val colorIndicator = itemView.findViewById(R.id.colorIndicator) val itemName = itemView.findViewById(R.id.itemName) diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/TagIconView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/TagIconView.kt new file mode 100644 index 0000000..d1963ea --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/TagIconView.kt @@ -0,0 +1,250 @@ +package com.remax.visualnovel.widget + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.drawable.GradientDrawable +import android.util.AttributeSet +import android.util.TypedValue +import android.widget.LinearLayout +import androidx.core.content.ContextCompat +import androidx.core.content.withStyledAttributes +import androidx.core.view.isVisible +import com.remax.visualnovel.R +import com.remax.visualnovel.databinding.WidgetTagIconBinding +import com.remax.visualnovel.entity.response.HeartbeatLevelEnum +import com.remax.visualnovel.extension.getTemperatureTxt +import com.remax.visualnovel.extension.setMargin +import com.remax.visualnovel.extension.setSize +import com.remax.visualnovel.utils.spannablex.utils.dp +import com.remax.visualnovel.widget.uitoken.changeTextColor +import com.remax.visualnovel.widget.uitoken.changeTextStyle +import com.remax.visualnovel.widget.uitoken.getGradientDrawableOrientation +import com.remax.visualnovel.widget.uitoken.handleUIToken +import com.dylanc.viewbinding.nonreflection.inflate + +/** + * Created by HJW on 2023/8/2 + */ +@SuppressLint("SetTextI18n") +class TagIconView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : + LinearLayout(context, attrs, defStyleAttr) { + + private var binding: WidgetTagIconBinding? = null + + private var type = GRADIENT_PRIMARY + + private var margin = 8 + + init { + binding = inflate(WidgetTagIconBinding::inflate) + + context.withStyledAttributes(attrs, R.styleable.TagIconView) { + val content = getString(R.styleable.TagIconView_tagIconContent) + val iconFont = getString(R.styleable.TagIconView_tagIconFont) + type = getInt(R.styleable.TagIconView_tagIconType, GRADIENT_PRIMARY) + val size = getInt(R.styleable.TagIconView_tagIconSize, SIZE_L) + + setTagContent(content, iconFont) + setTagIconType(type) + + var iconSize = 16f + + var txtToken = R.string.txt_label_m + +// var groupPaddingV = 6.dp +// var groupPaddingH = 8.dp + + var groupHeight = 32.dp + + when (size) { + SIZE_L -> { + + } + + SIZE_M -> { + iconSize = 12f + margin = 4 + txtToken = R.string.txt_label_s + +// groupPaddingV = 2.dp +// groupPaddingH = 4.dp + + groupHeight = 24.dp + } + + SIZE_S -> { + iconSize = 12f + margin = 4 + txtToken = R.string.txt_label_s +// +// groupPaddingV = 2.dp +// groupPaddingH = 2.dp + + groupHeight = 20.dp + } + } + + binding?.run { + group.setSize(height = groupHeight) +// group.setPadding(groupPaddingH, groupPaddingV, groupPaddingH, groupPaddingV) + + with(textView) { + changeTextStyle { + textUITextToken = context.getString(txtToken) + } + setMargin(marginStart = margin.dp, marginEnd = margin.dp) + } + + with(iconView) { + setTextSize(TypedValue.COMPLEX_UNIT_SP, iconSize) + setMargin(marginStart = margin.dp) + } + + } + } + } + + fun getTagTextView() = binding!!.textView + + fun setTagContent(content: String?, iconFont: String? = null) { + binding?.run { + textView.text = content + + iconView.text = iconFont + iconView.isVisible = iconFont != null + } + } + + fun setHeartbeatTag(heartbeatLevel: String? = null, heartbeatVal: Double? = null, showLevel: Boolean): String { + var returnTxt = "" + binding?.run { + iconView.isVisible = false + // 关系 + val currLevel = + HeartbeatLevelEnum.entries.find { it.levelName == heartbeatLevel } + val tagName = if (showLevel && currLevel != null) context.getString(currLevel.tagName) else null + + // 温度值,传入null就只显示关系 + val temperatureTxt = heartbeatVal?.getTemperatureTxt() + + returnTxt = listOfNotNull(tagName, temperatureTxt).joinToString(" · ") + if (returnTxt.isNotEmpty()){ + returnTxt = " $returnTxt " + } + val tvWidth = textView.paint.measureText(returnTxt) + 1.dp + textView.setSize(width = tvWidth.toInt()) + textView.text = returnTxt + + val currType = when (currLevel) { + HeartbeatLevelEnum.LEVEL_3, HeartbeatLevelEnum.LEVEL_4 -> VIOLET + HeartbeatLevelEnum.LEVEL_5, HeartbeatLevelEnum.LEVEL_6 -> ORANGE + HeartbeatLevelEnum.LEVEL_7, HeartbeatLevelEnum.LEVEL_8 -> MAGENTA + HeartbeatLevelEnum.LEVEL_9, HeartbeatLevelEnum.LEVEL_10 -> GRADIENT_PRIMARY + else -> ELEMENT_DARK + } + if (currType != type) { + setTagIconType(currType) + } + } + return returnTxt + } + + + fun setTagIconType(type: Int) { + this.type = type + binding?.run { + var textColorToken = context.getString(R.string.color_txt_primary_normal) + var blurColor = ContextCompat.getColor(context, R.color.white_p15) + var groupColorToken = R.color.white_p15 + + group.setBackgroundResource(R.color.transparent) + + when (type) { + ELEMENT -> { + blurColor = ContextCompat.getColor(context, R.color.glo_color_purple_0_p8) + } + + ELEMENT_LIGHT -> { + blurColor = ContextCompat.getColor(context, R.color.white_p15) + } + + ELEMENT_DARK -> { + blurColor = ContextCompat.getColor(context, R.color.black_p65) + } + + MINT -> { + group.setBackgroundResource(R.color.glo_color_mint_40_p60) + } + + BLUE -> { + group.setBackgroundResource(R.color.glo_color_blue_40_p60) + } + + VIOLET -> { + group.setBackgroundResource(R.color.glo_color_violet_40_p60) + } + + MAGENTA -> { + group.setBackgroundResource(R.color.glo_color_magenta_50_p60) + } + + ORANGE -> { + group.setBackgroundResource(R.color.glo_color_orange_50_p60) + } + + GRADIENT_PRIMARY -> { + context.handleUIToken(R.string.color_primary_gradient_normal)?.let { + val gradientDrawable = GradientDrawable() + gradientDrawable.shape = GradientDrawable.RECTANGLE + gradientDrawable.orientation = getGradientDrawableOrientation(it.deg) + gradientDrawable.colors = it.colors + group.background = gradientDrawable + } + } + + GRADIENT_SECOND -> { + context.handleUIToken(R.string.color_context_vip_normal)?.let { + val gradientDrawable = GradientDrawable() + gradientDrawable.shape = GradientDrawable.RECTANGLE + gradientDrawable.orientation = getGradientDrawableOrientation(it.deg) + gradientDrawable.colors = it.colors + group.background = gradientDrawable + } + textColorToken = context.getString(R.string.color_background_default) + } + } + + blurView.setOverlayColor(blurColor) + + textView.changeTextColor { + textUIColorToken = textColorToken + } + iconView.changeTextColor { + textUIColorToken = textColorToken + } + } + } + + companion object { + const val ELEMENT = 1 + const val ELEMENT_LIGHT = 2 + const val ELEMENT_DARK = 3 + + const val MINT = 4 + const val BLUE = 5 + const val VIOLET = 6 + const val MAGENTA = 7 + const val ORANGE = 8 + + const val GRADIENT_PRIMARY = 9 + const val GRADIENT_SECOND = 10 + + const val SIZE_L = 1 + const val SIZE_M = 2 + const val SIZE_S = 3 + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/ShapeBlurView.java b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/ShapeBlurView.java new file mode 100644 index 0000000..35f58a6 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/ShapeBlurView.java @@ -0,0 +1,975 @@ +package com.remax.visualnovel.widget.blurview; + +import android.app.Activity; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +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.Shader; +import android.os.Build; +import android.util.AttributeSet; +import android.util.StateSet; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewTreeObserver; + +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.DimenRes; +import androidx.annotation.FloatRange; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +import com.remax.visualnovel.R; +import com.remax.visualnovel.widget.blurview.enu.BlurCorner; +import com.remax.visualnovel.widget.blurview.enu.BlurMode; +import com.remax.visualnovel.widget.blurview.impl.AndroidStockBlurImpl; +import com.remax.visualnovel.widget.blurview.impl.AndroidXBlurImpl; +import com.remax.visualnovel.widget.blurview.impl.BlurImpl; +import com.remax.visualnovel.widget.blurview.impl.EmptyBlurImpl; +import com.remax.visualnovel.widget.blurview.impl.SupportLibraryBlurImpl; + + +public class ShapeBlurView extends View { + + private Context mContext; + + /** + * default 4 + */ + private float mDownSampleFactor; + /** + * default #000000 + */ + private int mOverlayColor; + /** + * default 10dp (0 < r <= 25) + */ + private float mBlurRadius; + public static final int DEFAULT_BORDER_COLOR = Color.WHITE; + + private final BlurImpl mBlurImpl; + private boolean mDirty; + private Bitmap mBitmapToBlur, mBlurredBitmap; + private Canvas mBlurringCanvas; + private boolean mIsRendering; + + + private final Rect mRectSrc = new Rect(); + private final RectF mRectFDst = new RectF(); + /** + * mDecorView should be the root view of the activity (even if you are on a different window like a dialog) + */ + private View mDecorView; + /** + * If the view is on different root view (usually means we are on a PopupWindow), + * we need to manually call invalidate() in onPreDraw(), otherwise we will not be able to see the changes + */ + private boolean mDifferentRoot; + private static int RENDERING_COUNT; + private static int BLUR_IMPL; + + private int blurMode = BlurMode.MODE_RECTANGLE; + private final Paint mBitmapPaint; + //圆形 相关 + private float cx = 0, cy = 0, cRadius = 0; + + //圆角相关 + private static final float DEFAULT_RADIUS = 0f; + private final float[] mCornerRadii = new float[]{DEFAULT_RADIUS, DEFAULT_RADIUS, DEFAULT_RADIUS, DEFAULT_RADIUS}; + private final Path cornerPath = new Path(); + private float[] cornerRids; + + //边框相关 + private static final float DEFAULT_BORDER_WIDTH = 0f; + + private final RectF mBorderRect = new RectF(); + private final Paint mBorderPaint; + private float mBorderWidth = 0; + private ColorStateList mBorderColor = ColorStateList.valueOf(DEFAULT_BORDER_COLOR); + private Matrix matrix; + private BitmapShader shader; + + public ShapeBlurView(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + // provide your own by override getBlurImpl() + mBlurImpl = getBlurImpl(); + try { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ShapeBlurView); + mBlurRadius = a.getDimension(R.styleable.ShapeBlurView_blur_radius, + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 25, context.getResources().getDisplayMetrics())); + mDownSampleFactor = a.getFloat(R.styleable.ShapeBlurView_blur_down_sample, 4); + mOverlayColor = a.getColor(R.styleable.ShapeBlurView_blur_overlay_color, 0xA6000000); + + float cornerRadiusOverride = + a.getDimensionPixelSize(R.styleable.ShapeBlurView_blur_corner_radius, -1); + mCornerRadii[BlurCorner.TOP_LEFT] = + a.getDimensionPixelSize(R.styleable.ShapeBlurView_blur_corner_radius_top_left, -1); + mCornerRadii[BlurCorner.TOP_RIGHT] = + a.getDimensionPixelSize(R.styleable.ShapeBlurView_blur_corner_radius_top_right, -1); + mCornerRadii[BlurCorner.BOTTOM_RIGHT] = + a.getDimensionPixelSize(R.styleable.ShapeBlurView_blur_corner_radius_bottom_right, -1); + mCornerRadii[BlurCorner.BOTTOM_LEFT] = + a.getDimensionPixelSize(R.styleable.ShapeBlurView_blur_corner_radius_bottom_left, -1); + initCornerData(cornerRadiusOverride); + blurMode = a.getInt(R.styleable.ShapeBlurView_blur_mode, BlurMode.MODE_RECTANGLE); + + mBorderWidth = a.getDimensionPixelSize(R.styleable.ShapeBlurView_blur_border_width, -1); + if (mBorderWidth < 0) { + mBorderWidth = DEFAULT_BORDER_WIDTH; + } + mBorderColor = a.getColorStateList(R.styleable.ShapeBlurView_blur_border_color); + if (mBorderColor == null) { + mBorderColor = ColorStateList.valueOf(DEFAULT_BORDER_COLOR); + } + + + a.recycle(); + } catch (Exception e) { + e.printStackTrace(); + } + mBitmapPaint = new Paint(); +// mBitmapPaint.setStyle(Paint.Style.FILL); + mBitmapPaint.setAntiAlias(true); + + mBorderPaint = new Paint(); + mBorderPaint.setStyle(Paint.Style.STROKE); + mBorderPaint.setAntiAlias(true); + mBorderPaint.setColor(mBorderColor.getColorForState(getState(), DEFAULT_BORDER_COLOR)); + mBorderPaint.setStrokeWidth(mBorderWidth); + +// matrix = new Matrix(); + } + + public void initCornerData(float cornerRadiusOverride) { + boolean any = false; + for (int i = 0, len = mCornerRadii.length; i < len; i++) { + if (mCornerRadii[i] < 0) { + mCornerRadii[i] = 0f; + } else { + any = true; + } + } + if (!any) { + if (cornerRadiusOverride < 0) { + cornerRadiusOverride = DEFAULT_RADIUS; + } + for (int i = 0, len = mCornerRadii.length; i < len; i++) { + mCornerRadii[i] = cornerRadiusOverride; + } + } + initCornerRids(); + } + + private void initCornerRids() { + if (cornerRids == null) { + cornerRids = new float[]{mCornerRadii[BlurCorner.TOP_LEFT], mCornerRadii[BlurCorner.TOP_LEFT], + mCornerRadii[BlurCorner.TOP_RIGHT], mCornerRadii[BlurCorner.TOP_RIGHT], + mCornerRadii[BlurCorner.BOTTOM_RIGHT], mCornerRadii[BlurCorner.BOTTOM_RIGHT], + mCornerRadii[BlurCorner.BOTTOM_LEFT], mCornerRadii[BlurCorner.BOTTOM_LEFT]}; + } else { + cornerRids[0] = mCornerRadii[BlurCorner.TOP_LEFT]; + cornerRids[1] = mCornerRadii[BlurCorner.TOP_LEFT]; + cornerRids[2] = mCornerRadii[BlurCorner.TOP_RIGHT]; + cornerRids[3] = mCornerRadii[BlurCorner.TOP_RIGHT]; + cornerRids[4] = mCornerRadii[BlurCorner.BOTTOM_RIGHT]; + cornerRids[5] = mCornerRadii[BlurCorner.BOTTOM_RIGHT]; + cornerRids[6] = mCornerRadii[BlurCorner.BOTTOM_LEFT]; + cornerRids[7] = mCornerRadii[BlurCorner.BOTTOM_LEFT]; + } + } + + protected BlurImpl getBlurImpl() { + if (BLUR_IMPL == 0) { + // try to use stock impl first + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + try { + AndroidStockBlurImpl impl = new AndroidStockBlurImpl(); + Bitmap bmp = Bitmap.createBitmap(4, 4, Bitmap.Config.ARGB_8888); + impl.prepare(getContext(), bmp, 4); + impl.release(); + bmp.recycle(); + BLUR_IMPL = 3; + } catch (Throwable e) { + } + } + } + if (BLUR_IMPL == 0) { + try { + getClass().getClassLoader().loadClass("androidx.renderscript.RenderScript"); + // initialize RenderScript to load jni impl + // may throw unsatisfied link error + AndroidXBlurImpl impl = new AndroidXBlurImpl(); + Bitmap bmp = Bitmap.createBitmap(4, 4, Bitmap.Config.ARGB_8888); + impl.prepare(getContext(), bmp, 4); + impl.release(); + bmp.recycle(); + BLUR_IMPL = 1; + } catch (Throwable e) { + // class not found or unsatisfied link + } + } + if (BLUR_IMPL == 0) { + try { + getClass().getClassLoader().loadClass("android.support.v8.renderscript.RenderScript"); + // initialize RenderScript to load jni impl + // may throw unsatisfied link error + SupportLibraryBlurImpl impl = new SupportLibraryBlurImpl(); + Bitmap bmp = Bitmap.createBitmap(4, 4, Bitmap.Config.ARGB_8888); + impl.prepare(getContext(), bmp, 4); + impl.release(); + bmp.recycle(); + BLUR_IMPL = 2; + } catch (Throwable e) { + // class not found or unsatisfied link + } + } + if (BLUR_IMPL == 0) { + // fallback to empty impl, which doesn't have blur effect + BLUR_IMPL = -1; + } + switch (BLUR_IMPL) { + case 1: + return new AndroidXBlurImpl(); + case 2: + return new SupportLibraryBlurImpl(); + case 3: + return new AndroidStockBlurImpl(); + default: + return new EmptyBlurImpl(); + } + } + +// public void setBlurRadius(@FloatRange(from = 0, to = 25) float radius) { +// if (mBlurRadius != radius) { +// mBlurRadius = radius; +// mDirty = true; +// invalidate(); +// } +// } + +// public void setDownSampleFactor(float factor) { +// if (factor <= 0) { +// throw new IllegalArgumentException("DownSample factor must be greater than 0."); +// } +// if (mDownSampleFactor != factor) { +// mDownSampleFactor = factor; +// // may also change blur radius +// mDirty = true; +// releaseBitmap(); +// invalidate(); +// } +// } + + public void setOverlayColor(int color) { + if (mOverlayColor != color) { + mOverlayColor = color; + invalidate(); + } + } + + /** + * Set all the corner radii from a dimension resource id. + * + * @param resId dimension resource id of radii. + */ +// public void setCornerRadiusDimen(@DimenRes int resId) { +// float radius = getResources().getDimension(resId); +// setCornerRadius(radius, radius, radius, radius); +// } + + /** + * Set the corner radius of a specific corner in px. + * + * @param radius + */ +// public void setCornerRadius(float radius) { +// setCornerRadius(radius, radius, radius, radius); +// } + + /** + * Set the corner radius of a specific corner in px. + */ +// public void setCornerRadius(@BlurCorner int corner, float radius) { +// if (mCornerRadii[corner] == radius) { +// return; +// } +// mCornerRadii[corner] = radius; +// initCornerRids(); +// invalidate(); +// } + + /** + * Set the corner radius of a specific corner in px. + */ +// public void setCornerRadius(float topLeft, float topRight, float bottomLeft, float bottomRight) { +// if (mCornerRadii[BlurCorner.TOP_LEFT] == topLeft +// && mCornerRadii[BlurCorner.TOP_RIGHT] == topRight +// && mCornerRadii[BlurCorner.BOTTOM_RIGHT] == bottomRight +// && mCornerRadii[BlurCorner.BOTTOM_LEFT] == bottomLeft) { +// return; +// } +// mCornerRadii[BlurCorner.TOP_LEFT] = topLeft; +// mCornerRadii[BlurCorner.TOP_RIGHT] = topRight; +// mCornerRadii[BlurCorner.BOTTOM_LEFT] = bottomLeft; +// mCornerRadii[BlurCorner.BOTTOM_RIGHT] = bottomRight; +// initCornerRids(); +// invalidate(); +// } + + /** + * @return the largest corner radius. + */ + public float getCornerRadius() { + return getMaxCornerRadius(); + } + + /** + * @return the largest corner radius. + */ + public float getMaxCornerRadius() { + float maxRadius = 0; + for (float r : mCornerRadii) { + maxRadius = Math.max(r, maxRadius); + } + return maxRadius; + } + + + public float getBorderWidth() { + return mBorderWidth; + } + +// public void setBorderWidth(@DimenRes int resId) { +// setBorderWidth(getResources().getDimension(resId)); +// } + +// public void setBorderWidth(float width) { +// if (mBorderWidth == width) { +// return; +// } +// mBorderWidth = width; +// invalidate(); +// } + + @ColorInt + public int getBorderColor() { + return mBorderColor.getDefaultColor(); + } + +// public void setBorderColor(@ColorInt int color) { +// setBorderColor(ColorStateList.valueOf(color)); +// } + +// public void setBorderColor(ColorStateList colors) { +// if (mBorderColor.equals(colors)) { +// return; +// } +// mBorderColor = (colors != null) ? colors : ColorStateList.valueOf(DEFAULT_BORDER_COLOR); +// mBorderPaint.setColor(mBorderColor.getColorForState(getState(), DEFAULT_BORDER_COLOR)); +// if (mBorderWidth > 0) { +// invalidate(); +// } +// } + + @BlurMode + public int getBlurMode() { + return this.blurMode; + } + +// public void setBlurMode(@BlurMode int blurMode) { +// if (this.blurMode == blurMode) { +// return; +// } +// this.blurMode = blurMode; +// invalidate(); +// } + + private void releaseBitmap() { + if (mBitmapToBlur != null) { + mBitmapToBlur.recycle(); + mBitmapToBlur = null; + } + if (mBlurredBitmap != null) { + mBlurredBitmap.recycle(); + mBlurredBitmap = null; + } + if (matrix != null) { + matrix = null; + } + if (shader != null) { + shader = null; + } + mContext = null; + } + + protected void release() { + releaseBitmap(); + mBlurImpl.release(); + } + + protected boolean prepare() { + if (mBlurRadius == 0) { + release(); + return false; + } + float downSampleFactor = mDownSampleFactor; + float radius = mBlurRadius / downSampleFactor; + if (radius > 25) { + downSampleFactor = downSampleFactor * radius / 25; + radius = 25; + } + final int width = getWidth(); + final int height = getHeight(); + int scaledWidth = Math.max(1, (int) (width / downSampleFactor)); + int scaledHeight = Math.max(1, (int) (height / downSampleFactor)); + boolean dirty = mDirty; + if (mBlurringCanvas == null || mBlurredBitmap == null + || mBlurredBitmap.getWidth() != scaledWidth + || mBlurredBitmap.getHeight() != scaledHeight) { + dirty = true; + releaseBitmap(); + boolean r = false; + try { + mBitmapToBlur = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888); + if (mBitmapToBlur == null) { + return false; + } + mBlurringCanvas = new Canvas(mBitmapToBlur); + mBlurredBitmap = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888); + if (mBlurredBitmap == null) { + return false; + } + r = true; + } catch (OutOfMemoryError e) { + // Bitmap.createBitmap() may cause OOM error + // Simply ignore and fallback + } finally { + if (!r) { + release(); + return false; + } + } + } + if (dirty) { + if (mBlurImpl.prepare(getContext(), mBitmapToBlur, radius)) { + mDirty = false; + } else { + return false; + } + } + return true; + } + + protected void blur(Bitmap bitmapToBlur, Bitmap blurredBitmap) { + shader = new BitmapShader(blurredBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + mBlurImpl.blur(bitmapToBlur, blurredBitmap); + } + + private final ViewTreeObserver.OnPreDrawListener preDrawListener = new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + final int[] locations = new int[2]; + Bitmap oldBmp = mBlurredBitmap; + View decor = mDecorView; + if (decor != null && isShown() && prepare()) { + boolean redrawBitmap = mBlurredBitmap != oldBmp; + oldBmp = null; + decor.getLocationOnScreen(locations); + int x = -locations[0]; + int y = -locations[1]; + getLocationOnScreen(locations); + x += locations[0]; + y += locations[1]; + // just erase transparent + mBitmapToBlur.eraseColor(mOverlayColor & 0xffffff); + int rc = mBlurringCanvas.save(); + mIsRendering = true; + RENDERING_COUNT++; + try { + mBlurringCanvas.scale(1.f * mBitmapToBlur.getWidth() / getWidth(), 1.f * mBitmapToBlur.getHeight() / getHeight()); + mBlurringCanvas.translate(-x, -y); + if (decor.getBackground() != null) { + decor.getBackground().draw(mBlurringCanvas); + } + decor.draw(mBlurringCanvas); + } catch (StopException e) { + } finally { + mIsRendering = false; + RENDERING_COUNT--; + mBlurringCanvas.restoreToCount(rc); + } + blur(mBitmapToBlur, mBlurredBitmap); + if (redrawBitmap || mDifferentRoot) { + invalidate(); + } + } + + return true; + } + }; + + protected View getActivityDecorView() { + Context ctx = getContext(); + for (int i = 0; i < 4 && !(ctx instanceof Activity) && ctx instanceof ContextWrapper; i++) { + ctx = ((ContextWrapper) ctx).getBaseContext(); + } + if (ctx instanceof Activity) { + return ((Activity) ctx).getWindow().getDecorView(); + } else { + return null; + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mDecorView = getActivityDecorView(); + if (mDecorView != null) { + mDecorView.getViewTreeObserver().addOnPreDrawListener(preDrawListener); + mDifferentRoot = mDecorView.getRootView() != getRootView(); + if (mDifferentRoot) { + mDecorView.postInvalidate(); + } + } else { + mDifferentRoot = false; + } + } + + @Override + protected void onDetachedFromWindow() { + if (mDecorView != null) { + mDecorView.getViewTreeObserver().removeOnPreDrawListener(preDrawListener); + } + release(); + super.onDetachedFromWindow(); + } + + @Override + public void draw(Canvas canvas) { + if (mIsRendering) { + // Quit here, don't draw views above me + throw STOP_EXCEPTION; + } else if (RENDERING_COUNT > 0) { + // Doesn't support blurview overlap on another blurview + } else { + super.draw(canvas); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + drawBlurredBitmap(canvas, mBlurredBitmap, mOverlayColor); + } + + /** + * Custom draw the blurred bitmap and color to define your own shape + * + * @param canvas + * @param blurBitmap + * @param overlayColor + */ + protected void drawBlurredBitmap(Canvas canvas, Bitmap blurBitmap, int overlayColor) { + if (blurBitmap != null) { + if (blurMode == BlurMode.MODE_CIRCLE) { + drawCircleRectBitmap(canvas, blurBitmap, overlayColor); + } else if (blurMode == BlurMode.MODE_OVAL) { + drawOvalRectBitmap(canvas, blurBitmap, overlayColor); + } else { + drawRoundRectBitmap(canvas, blurBitmap, overlayColor); + } + } + } + + /** + * 默认或者画矩形可带圆角 + * + * @param canvas + * @param blurBitmap + * @param overlayColor + */ + private void drawRoundRectBitmap(Canvas canvas, Bitmap blurBitmap, int overlayColor) { + try { + //圆角的半径,依次为左上角xy半径,右上角,右下角,左下角 + mRectFDst.right = getWidth(); + mRectFDst.bottom = getHeight(); + /*向路径中添加圆角矩形。radii数组定义圆角矩形的四个圆角的x,y半径。radii长度必须为8*/ + //Path.Direction.CW:clockwise ,沿顺时针方向绘制,Path.Direction.CCW:counter-clockwise ,沿逆时针方向绘制 + cornerPath.addRoundRect(mRectFDst, cornerRids, Path.Direction.CW); + cornerPath.close(); + canvas.clipPath(cornerPath); + + mRectSrc.right = blurBitmap.getWidth(); + mRectSrc.bottom = blurBitmap.getHeight(); + canvas.drawBitmap(blurBitmap, mRectSrc, mRectFDst, null); + mBitmapPaint.setColor(overlayColor); + canvas.drawRect(mRectFDst, mBitmapPaint); + if (mBorderWidth > 0) { + //目前没找到合适方式 + mBorderPaint.setStrokeWidth(mBorderWidth * 2); + canvas.drawPath(cornerPath, mBorderPaint); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 画椭圆,如果宽高一样则为圆形 + * + * @param canvas + * @param blurBitmap + * @param overlayColor + */ + private void drawOvalRectBitmap(Canvas canvas, Bitmap blurBitmap, int overlayColor) { + try { + mRectFDst.right = getWidth(); + mRectFDst.bottom = getHeight(); + mBitmapPaint.reset(); + mBitmapPaint.setAntiAlias(true); + if (shader == null) { + shader = new BitmapShader(blurBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + } + if (matrix == null) { + matrix = new Matrix(); + } + matrix.postScale(mRectFDst.width() / blurBitmap.getWidth(), mRectFDst.height() / blurBitmap.getHeight()); + shader.setLocalMatrix(matrix); + mBitmapPaint.setShader(shader); + canvas.drawOval(mRectFDst, mBitmapPaint); + mBitmapPaint.reset(); + mBitmapPaint.setAntiAlias(true); + mBitmapPaint.setColor(overlayColor); + canvas.drawOval(mRectFDst, mBitmapPaint); + if (mBorderWidth > 0) { + mBorderRect.set(mRectFDst); + mBorderRect.inset(mBorderWidth / 2, mBorderWidth / 2); + canvas.drawOval(mBorderRect, mBorderPaint); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 画圆形,以宽高最小的为半径 + * + * @param canvas + * @param blurBitmap + * @param overlayColor + */ + private void drawCircleRectBitmap(Canvas canvas, Bitmap blurBitmap, int overlayColor) { + try { + mRectFDst.right = getMeasuredWidth(); + mRectFDst.bottom = getMeasuredHeight(); + mRectSrc.right = blurBitmap.getWidth(); + mRectSrc.bottom = blurBitmap.getHeight(); + mBitmapPaint.reset(); + mBitmapPaint.setAntiAlias(true); + if (shader == null) { + shader = new BitmapShader(blurBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + } + if (matrix == null) { + matrix = new Matrix(); + } + matrix.postScale(mRectFDst.width() / mRectSrc.width(), mRectFDst.height() / mRectSrc.height()); + shader.setLocalMatrix(matrix); + mBitmapPaint.setShader(shader); + //前面Scale,故判断以哪一个来取中心点和半径 + if (mRectFDst.width() >= mRectSrc.width()) { + cx = mRectFDst.width() / 2; + cy = mRectFDst.height() / 2; + //取宽高最小的为半径 + cRadius = Math.min(mRectFDst.width(), mRectFDst.height()) / 2; + mBorderRect.set(mRectFDst); + } else { + cx = mRectSrc.width() / 2f; + cy = mRectSrc.height() / 2f; + cRadius = Math.min(mRectSrc.width(), mRectSrc.height()) / 2f; + mBorderRect.set(mRectSrc); + } + canvas.drawCircle(cx, cy, cRadius, mBitmapPaint); + mBitmapPaint.reset(); + mBitmapPaint.setAntiAlias(true); + mBitmapPaint.setColor(overlayColor); + canvas.drawCircle(cx, cy, cRadius, mBitmapPaint); + //使用宽高相等的椭圆为圆形来画边框 + if (mBorderWidth > 0) { + if (mBorderRect.width() > mBorderRect.height()) { + //原本宽大于高,圆是以中心点为圆心和高的一半为半径,椭圆区域是以初始00为开始,故整体向右移动差值 + float dif = Math.abs(mBorderRect.height() - mBorderRect.width()) / 2; + mBorderRect.left = dif; + mBorderRect.right = Math.min(mBorderRect.width(), mBorderRect.height()) + dif; + mBorderRect.bottom = Math.min(mBorderRect.width(), mBorderRect.height()); + } else if (mBorderRect.width() < mBorderRect.height()) { + //原本高大于宽,圆是以中心点为圆心和宽的一半为半径,椭圆区域是以初始00为开始,故整体向下移动差值 + float dif = Math.abs(mBorderRect.height() - mBorderRect.width()) / 2; + mBorderRect.top = dif; + mBorderRect.right = Math.min(mBorderRect.width(), mBorderRect.height()); + mBorderRect.bottom = Math.min(mBorderRect.width(), mBorderRect.height()) + dif; + } else { + //如果快高相同,则不需要偏移,椭圆画出来就是圆 + mBorderRect.right = Math.min(mBorderRect.width(), mBorderRect.height()); + mBorderRect.bottom = Math.min(mBorderRect.width(), mBorderRect.height()); + } + mBorderRect.inset(mBorderWidth / 2, mBorderWidth / 2); + canvas.drawOval(mBorderRect, mBorderPaint); + } + } catch (Exception e) { + e.printStackTrace(); + } + + } + + + /** + * dp转px + * + * @param dpValue dp值 + * @return px值 + */ + public int dp2px(final float dpValue) { + final float scale = mContext.getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + public @NonNull + int[] getState() { + return StateSet.WILD_CARD; + } + + private static class StopException extends RuntimeException { + } + + private static StopException STOP_EXCEPTION = new StopException(); + + /** + * 传入构造器,避免传统的设置一个参数调用一次invalidate()重新绘制 + * + * @return + */ + public void refreshView(Builder builder) { + boolean isInvalidate = false; + if (builder == null) { + return; + } + if (builder.blurMode != -1 && this.blurMode != builder.blurMode) { + this.blurMode = builder.blurMode; + isInvalidate = true; + } + if (builder.mBorderColor != null && !mBorderColor.equals(builder.mBorderColor)) { + this.mBorderColor = builder.mBorderColor; + mBorderPaint.setColor(mBorderColor.getColorForState(getState(), DEFAULT_BORDER_COLOR)); + if (mBorderWidth > 0) { + isInvalidate = true; + } + } + if (builder.mBorderWidth > 0) { + mBorderWidth = builder.mBorderWidth; + mBorderPaint.setStrokeWidth(mBorderWidth); + isInvalidate = true; + } + if (mCornerRadii[BlurCorner.TOP_LEFT] != builder.mCornerRadii[BlurCorner.TOP_LEFT] + || mCornerRadii[BlurCorner.TOP_RIGHT] != builder.mCornerRadii[BlurCorner.TOP_RIGHT] + || mCornerRadii[BlurCorner.BOTTOM_RIGHT] != builder.mCornerRadii[BlurCorner.BOTTOM_RIGHT] + || mCornerRadii[BlurCorner.BOTTOM_LEFT] != builder.mCornerRadii[BlurCorner.BOTTOM_LEFT]) { + mCornerRadii[BlurCorner.TOP_LEFT] = builder.mCornerRadii[BlurCorner.TOP_LEFT]; + mCornerRadii[BlurCorner.TOP_RIGHT] = builder.mCornerRadii[BlurCorner.TOP_RIGHT]; + mCornerRadii[BlurCorner.BOTTOM_LEFT] = builder.mCornerRadii[BlurCorner.BOTTOM_LEFT]; + mCornerRadii[BlurCorner.BOTTOM_RIGHT] = builder.mCornerRadii[BlurCorner.BOTTOM_RIGHT]; + isInvalidate = true; + initCornerRids(); + } + if (builder.mOverlayColor != -1 && mOverlayColor != builder.mOverlayColor) { + mOverlayColor = builder.mOverlayColor; + isInvalidate = true; + } + if (builder.mBlurRadius > 0 && mBlurRadius != builder.mBlurRadius) { + mBlurRadius = builder.mBlurRadius; + mDirty = true; + isInvalidate = true; + } + if (builder.mDownSampleFactor > 0 && mDownSampleFactor != builder.mDownSampleFactor) { + mDownSampleFactor = builder.mDownSampleFactor; + mDirty = true; + isInvalidate = true; + releaseBitmap(); + } + if (isInvalidate) { + invalidate(); + } + } + + public static class Builder { + + // default 4 + private float mDownSampleFactor = -1; + // default #aaffffff + private int mOverlayColor = -1; + // default 10dp (0 < r <= 25) + private float mBlurRadius = -1; + private float mBorderWidth = -1; + private ColorStateList mBorderColor = null; + private int blurMode = -1; + private final float[] mCornerRadii = new float[]{0f, 0f, 0f, 0f}; + private Context mContext; + + private Builder(Context context) { + mContext = context.getApplicationContext(); + } + + /** + * 模糊半径 + * + * @param radius 0~25 + * @return + */ + public Builder setBlurRadius(@FloatRange(from = 0, to = 25) float radius) { + mBlurRadius = radius; + return this; + } + + /** + * 采样率 + * + * @param factor + * @return + */ + public Builder setDownSampleFactor(float factor) { + if (factor <= 0) { + throw new IllegalArgumentException("DownSample factor must be greater than 0."); + } + mDownSampleFactor = factor; + return this; + } + + /** + * 蒙层颜色 + * + * @param color + * @return + */ + public Builder setOverlayColor(int color) { + mOverlayColor = color; + return this; + } + + /** + * Set the corner radius of a specific corner in px. + * 设置圆角 圆形、椭圆无效 + * + * @param corner 枚举类型 对应4个角 + * @param radius 角半径幅度 + * @return + */ + public Builder setCornerRadius(@BlurCorner int corner, float radius) { + mCornerRadii[corner] = radius; + return this; + } + + /** + * Set all the corner radii from a dimension resource id. + * 设置圆角 圆形、椭圆无效 + * + * @param resId dimension resource id of radii. + */ + public Builder setCornerRadiusDimen(@DimenRes int resId) { + float radius = mContext.getResources().getDimension(resId); + return setCornerRadius(radius, radius, radius, radius); + } + + /** + * Set the corner radius of a specific corner in px. + * 设置圆角 圆形、椭圆无效 + * + * @param radius 4个角同值 + */ + public Builder setCornerRadius(float radius) { + return setCornerRadius(radius, radius, radius, radius); + } + + /** + * Set the corner radius of a specific corner in px. + * 设置圆角 圆形、椭圆无效 + */ + public Builder setCornerRadius(float topLeft, float topRight, float bottomLeft, float bottomRight) { + mCornerRadii[BlurCorner.TOP_LEFT] = topLeft; + mCornerRadii[BlurCorner.TOP_RIGHT] = topRight; + mCornerRadii[BlurCorner.BOTTOM_LEFT] = bottomLeft; + mCornerRadii[BlurCorner.BOTTOM_RIGHT] = bottomRight; + return this; + } + + /** + * 设置边框的宽度 + * + * @param resId + * @return + */ + public Builder setBorderWidth(@DimenRes int resId) { + return setBorderWidth(mContext.getResources().getDimension(resId)); + } + + /** + * 设置边框的宽度 + * + * @param width 转px值 + * @return + */ + public Builder setBorderWidth(float width) { + mBorderWidth = width; + return this; + } + + /** + * 设置边框颜色 + * + * @param color R.color.xxxx + * @return + */ + public Builder setBorderColor(@ColorRes int color) { + return setBorderColor(ColorStateList.valueOf(ContextCompat.getColor(mContext, color))); + } + +// public Builder setBorderColor(@ColorInt int color) { +// return setBorderColor(ColorStateList.valueOf(color)); +// } + + public Builder setBorderColor(ColorStateList colors) { + mBorderColor = (colors != null) ? colors : ColorStateList.valueOf(DEFAULT_BORDER_COLOR); + return this; + } + + /** + * 设置高斯模糊的类型 + * + * @param blurMode BlurMode枚举值,支持圆、方形、椭圆(宽高相等椭圆为圆) + * @return + */ + public Builder setBlurMode(@BlurMode int blurMode) { + this.blurMode = blurMode; + return this; + } + + } + + /** + * 建造者模式,避免设置一个参数调用一次重新绘制 + * + * @return + */ + public static Builder build(Context context) { + return new Builder(context); + } +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/enu/BlurCorner.java b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/enu/BlurCorner.java new file mode 100644 index 0000000..11081bd --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/enu/BlurCorner.java @@ -0,0 +1,18 @@ +package com.remax.visualnovel.widget.blurview.enu; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.SOURCE) +@IntDef({ + BlurCorner.TOP_LEFT, BlurCorner.TOP_RIGHT, + BlurCorner.BOTTOM_LEFT, BlurCorner.BOTTOM_RIGHT +}) +public @interface BlurCorner { + int TOP_LEFT = 0; + int TOP_RIGHT = 1; + int BOTTOM_RIGHT = 2; + int BOTTOM_LEFT = 3; +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/enu/BlurMode.java b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/enu/BlurMode.java new file mode 100644 index 0000000..e3123e7 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/enu/BlurMode.java @@ -0,0 +1,18 @@ +package com.remax.visualnovel.widget.blurview.enu; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.SOURCE) +@IntDef({ + BlurMode.MODE_RECTANGLE, BlurMode.MODE_CIRCLE, + BlurMode.MODE_OVAL +}) + +public @interface BlurMode { + int MODE_RECTANGLE = 0; + int MODE_CIRCLE = 1; + int MODE_OVAL = 2; +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/impl/AndroidStockBlurImpl.java b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/impl/AndroidStockBlurImpl.java new file mode 100644 index 0000000..12ce153 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/impl/AndroidStockBlurImpl.java @@ -0,0 +1,79 @@ +package com.remax.visualnovel.widget.blurview.impl; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.graphics.Bitmap; +import android.renderscript.Allocation; +import android.renderscript.Element; +import android.renderscript.RenderScript; +import android.renderscript.ScriptIntrinsicBlur; + +public class AndroidStockBlurImpl implements BlurImpl{ + + private RenderScript mRenderScript; + private ScriptIntrinsicBlur mBlurScript; + private Allocation mBlurInput, mBlurOutput; + + @Override + public boolean prepare(Context context, Bitmap buffer, float radius) { + if (mRenderScript == null) { + try { + mRenderScript = RenderScript.create(context); + mBlurScript = ScriptIntrinsicBlur.create(mRenderScript, Element.U8_4(mRenderScript)); + } catch (android.renderscript.RSRuntimeException e) { + if (isDebug(context)) { + throw e; + } else { + // In release mode, just ignore + release(); + return false; + } + } + } + mBlurScript.setRadius(radius); + + mBlurInput = Allocation.createFromBitmap(mRenderScript, buffer, + Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT); + mBlurOutput = Allocation.createTyped(mRenderScript, mBlurInput.getType()); + + return true; + } + + @Override + public void release() { + if (mBlurInput != null) { + mBlurInput.destroy(); + mBlurInput = null; + } + if (mBlurOutput != null) { + mBlurOutput.destroy(); + mBlurOutput = null; + } + if (mBlurScript != null) { + mBlurScript.destroy(); + mBlurScript = null; + } + if (mRenderScript != null) { + mRenderScript.destroy(); + mRenderScript = null; + } + } + + @Override + public void blur(Bitmap input, Bitmap output) { + mBlurInput.copyFrom(input); + mBlurScript.setInput(mBlurInput); + mBlurScript.forEach(mBlurOutput); + mBlurOutput.copyTo(output); + } + + // android:debuggable="true" in AndroidManifest.xml (auto set by build tool) + static Boolean DEBUG = null; + + static boolean isDebug(Context ctx) { + if (DEBUG == null && ctx != null) { + DEBUG = (ctx.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; + } + return DEBUG.equals(Boolean.TRUE); + } +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/impl/AndroidXBlurImpl.java b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/impl/AndroidXBlurImpl.java new file mode 100644 index 0000000..ff7eb93 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/impl/AndroidXBlurImpl.java @@ -0,0 +1,79 @@ +package com.remax.visualnovel.widget.blurview.impl; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.graphics.Bitmap; +import android.renderscript.Allocation; +import android.renderscript.Element; +import android.renderscript.RenderScript; +import android.renderscript.ScriptIntrinsicBlur; + +public class AndroidXBlurImpl implements BlurImpl { + private RenderScript mRenderScript; + private ScriptIntrinsicBlur mBlurScript; + private Allocation mBlurInput, mBlurOutput; + + @Override + public boolean prepare(Context context, Bitmap buffer, float radius) { + if (mRenderScript == null) { + try { + mRenderScript = RenderScript.create(context); + mBlurScript = ScriptIntrinsicBlur.create(mRenderScript, Element.U8_4(mRenderScript)); + } catch (android.renderscript.RSRuntimeException e) { + if (isDebug(context)) { + throw e; + } else { + // In release mode, just ignore + release(); + return false; + } + } + } + mBlurScript.setRadius(radius); + + mBlurInput = Allocation.createFromBitmap(mRenderScript, buffer, + Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT); + mBlurOutput = Allocation.createTyped(mRenderScript, mBlurInput.getType()); + + return true; + } + + @Override + public void release() { + if (mBlurInput != null) { + mBlurInput.destroy(); + mBlurInput = null; + } + if (mBlurOutput != null) { + mBlurOutput.destroy(); + mBlurOutput = null; + } + if (mBlurScript != null) { + mBlurScript.destroy(); + mBlurScript = null; + } + if (mRenderScript != null) { + mRenderScript.destroy(); + mRenderScript = null; + } + } + + @Override + public void blur(Bitmap input, Bitmap output) { + mBlurInput.copyFrom(input); + mBlurScript.setInput(mBlurInput); + mBlurScript.forEach(mBlurOutput); + mBlurOutput.copyTo(output); + } + + // android:debuggable="true" in AndroidManifest.xml (auto set by build tool) + static Boolean DEBUG = null; + + static boolean isDebug(Context ctx) { + if (DEBUG == null && ctx != null) { + DEBUG = (ctx.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; + } + return DEBUG.equals(Boolean.TRUE); + } + +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/impl/BlurImpl.java b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/impl/BlurImpl.java new file mode 100644 index 0000000..a996782 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/impl/BlurImpl.java @@ -0,0 +1,13 @@ +package com.remax.visualnovel.widget.blurview.impl; + +import android.content.Context; +import android.graphics.Bitmap; + +public interface BlurImpl { + + boolean prepare(Context context, Bitmap buffer, float radius); + + void release(); + + void blur(Bitmap input, Bitmap output); +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/impl/EmptyBlurImpl.java b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/impl/EmptyBlurImpl.java new file mode 100644 index 0000000..7341ca4 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/impl/EmptyBlurImpl.java @@ -0,0 +1,23 @@ +package com.remax.visualnovel.widget.blurview.impl; + +import android.content.Context; +import android.graphics.Bitmap; + +public class EmptyBlurImpl implements BlurImpl { + + @Override + public boolean prepare(Context context, Bitmap buffer, float radius) { + return false; + } + + @Override + public void release() { + + } + + @Override + public void blur(Bitmap input, Bitmap output) { + + } + +} diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/impl/SupportLibraryBlurImpl.java b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/impl/SupportLibraryBlurImpl.java new file mode 100644 index 0000000..0b2f64a --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/widget/blurview/impl/SupportLibraryBlurImpl.java @@ -0,0 +1,79 @@ +package com.remax.visualnovel.widget.blurview.impl; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.graphics.Bitmap; +import android.renderscript.Allocation; +import android.renderscript.Element; +import android.renderscript.RenderScript; +import android.renderscript.ScriptIntrinsicBlur; + +public class SupportLibraryBlurImpl implements BlurImpl { + + private RenderScript mRenderScript; + private ScriptIntrinsicBlur mBlurScript; + private Allocation mBlurInput, mBlurOutput; + + @Override + public boolean prepare(Context context, Bitmap buffer, float radius) { + if (mRenderScript == null) { + try { + mRenderScript = RenderScript.create(context); + mBlurScript = ScriptIntrinsicBlur.create(mRenderScript, Element.U8_4(mRenderScript)); + } catch (android.renderscript.RSRuntimeException e) { + if (isDebug(context)) { + throw e; + } else { + // In release mode, just ignore + release(); + return false; + } + } + } + mBlurScript.setRadius(radius); + + mBlurInput = Allocation.createFromBitmap(mRenderScript, buffer, + Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT); + mBlurOutput = Allocation.createTyped(mRenderScript, mBlurInput.getType()); + + return true; + } + + @Override + public void release() { + if (mBlurInput != null) { + mBlurInput.destroy(); + mBlurInput = null; + } + if (mBlurOutput != null) { + mBlurOutput.destroy(); + mBlurOutput = null; + } + if (mBlurScript != null) { + mBlurScript.destroy(); + mBlurScript = null; + } + if (mRenderScript != null) { + mRenderScript.destroy(); + mRenderScript = null; + } + } + + @Override + public void blur(Bitmap input, Bitmap output) { + mBlurInput.copyFrom(input); + mBlurScript.setInput(mBlurInput); + mBlurScript.forEach(mBlurOutput); + mBlurOutput.copyTo(output); + } + + // android:debuggable="true" in AndroidManifest.xml (auto set by build tool) + static Boolean DEBUG = null; + + static boolean isDebug(Context ctx) { + if (DEBUG == null && ctx != null) { + DEBUG = (ctx.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; + } + return DEBUG.equals(Boolean.TRUE); + } +} diff --git a/VisualNovel/app/src/main/res/layout/layout_chat_menu_view.xml b/VisualNovel/app/src/main/res/layout/layout_chat_menu_view.xml index 722ec86..19f6c07 100644 --- a/VisualNovel/app/src/main/res/layout/layout_chat_menu_view.xml +++ b/VisualNovel/app/src/main/res/layout/layout_chat_menu_view.xml @@ -222,6 +222,18 @@ android:layout_height="wrap_content" /> + + + + diff --git a/VisualNovel/app/src/main/res/layout/layout_expand_view_item.xml b/VisualNovel/app/src/main/res/layout/layout_item_ai_model.xml similarity index 100% rename from VisualNovel/app/src/main/res/layout/layout_expand_view_item.xml rename to VisualNovel/app/src/main/res/layout/layout_item_ai_model.xml diff --git a/VisualNovel/app/src/main/res/layout/layout_item_chat_mode.xml b/VisualNovel/app/src/main/res/layout/layout_item_chat_mode.xml new file mode 100644 index 0000000..7a5bf0e --- /dev/null +++ b/VisualNovel/app/src/main/res/layout/layout_item_chat_mode.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VisualNovel/app/src/main/res/layout/layout_item_setting_bubble.xml b/VisualNovel/app/src/main/res/layout/layout_item_setting_bubble.xml index b606fd1..f0132f9 100644 --- a/VisualNovel/app/src/main/res/layout/layout_item_setting_bubble.xml +++ b/VisualNovel/app/src/main/res/layout/layout_item_setting_bubble.xml @@ -15,46 +15,12 @@ app:layout_constraintDimensionRatio="h,164:120" app:layout_constraintTop_toTopOf="parent" app:radiusToken="@string/radius_l" - app:strokeColorToken="@string/color_primary_normal" - app:strokeWidthToken="@string/border_m" + app:strokeColorToken="@string/color_txt_tertiary_normal" + app:strokeWidthToken="@string/border_l" /> - - - - - - - + + + + + /> - + android:src="@mipmap/chat_left_bg" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + /> \ No newline at end of file diff --git a/VisualNovel/app/src/main/res/layout/widget_tag_icon.xml b/VisualNovel/app/src/main/res/layout/widget_tag_icon.xml new file mode 100644 index 0000000..cce2d97 --- /dev/null +++ b/VisualNovel/app/src/main/res/layout/widget_tag_icon.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VisualNovel/app/src/main/res/mipmap-xxhdpi/setting_bubble_icon.webp b/VisualNovel/app/src/main/res/mipmap-xxhdpi/setting_bubble_icon.webp new file mode 100644 index 0000000..e483a90 Binary files /dev/null and b/VisualNovel/app/src/main/res/mipmap-xxhdpi/setting_bubble_icon.webp differ diff --git a/VisualNovel/app/src/main/res/mipmap-xxhdpi/setting_chat_mode_icon.webp b/VisualNovel/app/src/main/res/mipmap-xxhdpi/setting_chat_mode_icon.webp new file mode 100644 index 0000000..b9647cf Binary files /dev/null and b/VisualNovel/app/src/main/res/mipmap-xxhdpi/setting_chat_mode_icon.webp differ diff --git a/VisualNovel/app/src/main/res/values/strings.xml b/VisualNovel/app/src/main/res/values/strings.xml index 246fe9d..bd289aa 100644 --- a/VisualNovel/app/src/main/res/values/strings.xml +++ b/VisualNovel/app/src/main/res/values/strings.xml @@ -389,7 +389,6 @@ Hi %d to Create Create image - Unlocked Waiting to be connected Interrupt Listening @@ -481,5 +480,8 @@ Maximum number of response tokens Font Size Voice actor + Chat buttle + Unlocked + Chat Mode \ No newline at end of file