chat mode 选择器
This commit is contained in:
		
							parent
							
								
									293405c8d1
								
							
						
					
					
						commit
						7358fbd8e2
					
				|  | @ -6,11 +6,6 @@ import kotlinx.parcelize.Parcelize | ||||||
| 
 | 
 | ||||||
| @Parcelize | @Parcelize | ||||||
| data class ChatBubble( | data class ChatBubble( | ||||||
|     /** |  | ||||||
|      * code |  | ||||||
|      */ |  | ||||||
|     val code: String, |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * id |      * id | ||||||
|      */ |      */ | ||||||
|  | @ -40,7 +35,7 @@ data class ChatBubble( | ||||||
|      * 解锁类型 MEMBER:会员 HEARTBEAT_LEVEL:心动等级 |      * 解锁类型 MEMBER:会员 HEARTBEAT_LEVEL:心动等级 | ||||||
|      */ |      */ | ||||||
|     val unlockType: String? = null, |     val unlockType: String? = null, | ||||||
|     var isDefault: Boolean, |     var isDefault: Boolean = false, | ||||||
|     var select: Boolean = false |     var select: Boolean = false | ||||||
| ) : Parcelable { | ) : Parcelable { | ||||||
|     companion object { |     companion object { | ||||||
|  |  | ||||||
|  | @ -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, | ||||||
|  | ) | ||||||
|  | @ -9,6 +9,8 @@ import android.widget.LinearLayout | ||||||
| import androidx.core.graphics.toColorInt | import androidx.core.graphics.toColorInt | ||||||
| import com.remax.visualnovel.R | import com.remax.visualnovel.R | ||||||
| import com.remax.visualnovel.databinding.LayoutChatMenuViewBinding | 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.entity.response.ChatSound | ||||||
| import com.remax.visualnovel.ui.chat.ui.expandableSelector.SelectorItem | import com.remax.visualnovel.ui.chat.ui.expandableSelector.SelectorItem | ||||||
| 
 | 
 | ||||||
|  | @ -27,7 +29,9 @@ class ChatSettingView @JvmOverloads constructor( | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         initAiModelSelectorView() |         initAiModelSelectorView() | ||||||
|  |         initChatModeSelectorView() | ||||||
|         initSoundSelectorView() |         initSoundSelectorView() | ||||||
|  |         initBubbleSelectView() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -67,6 +71,34 @@ class ChatSettingView @JvmOverloads constructor( | ||||||
|         mBinding.aiModelSelector.selectItem(0) |         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() { |     fun initSoundSelectorView() { | ||||||
|         val items = listOf( |         val items = listOf( | ||||||
|             ChatSound( |             ChatSound( | ||||||
|  | @ -74,14 +106,14 @@ class ChatSettingView @JvmOverloads constructor( | ||||||
|                 name = "Sound-1", |                 name = "Sound-1", | ||||||
|                 description = "This is description for sound-1", |                 description = "This is description for sound-1", | ||||||
|                 isMale = true, |                 isMale = true, | ||||||
|                 imgUrl = "aa" |                 imgUrl = "" | ||||||
|             ), |             ), | ||||||
|             ChatSound( |             ChatSound( | ||||||
|                 id = 2L, |                 id = 2L, | ||||||
|                 name = "Sound-2", |                 name = "Sound-2", | ||||||
|                 description = "This is description for sound-2", |                 description = "This is description for sound-2", | ||||||
|                 isMale = true, |                 isMale = true, | ||||||
|                 imgUrl = "aa" |                 imgUrl = "" | ||||||
|             ), |             ), | ||||||
| 
 | 
 | ||||||
|             ChatSound( |             ChatSound( | ||||||
|  | @ -89,7 +121,7 @@ class ChatSettingView @JvmOverloads constructor( | ||||||
|                 name = "Sound-3", |                 name = "Sound-3", | ||||||
|                 description = "This is description for sound-3", |                 description = "This is description for sound-3", | ||||||
|                 isMale = true, |                 isMale = true, | ||||||
|                 imgUrl = "aa" |                 imgUrl = "" | ||||||
|             ), |             ), | ||||||
| 
 | 
 | ||||||
|             ChatSound( |             ChatSound( | ||||||
|  | @ -97,7 +129,7 @@ class ChatSettingView @JvmOverloads constructor( | ||||||
|                 name = "Sound-4", |                 name = "Sound-4", | ||||||
|                 description = "This is description for sound-4", |                 description = "This is description for sound-4", | ||||||
|                 isMale = true, |                 isMale = true, | ||||||
|                 imgUrl = "aa" |                 imgUrl = "" | ||||||
|             ), |             ), | ||||||
| 
 | 
 | ||||||
|             ChatSound( |             ChatSound( | ||||||
|  | @ -105,12 +137,47 @@ class ChatSettingView @JvmOverloads constructor( | ||||||
|                 name = "Sound-5", |                 name = "Sound-5", | ||||||
|                 description = "This is description for sound-5", |                 description = "This is description for sound-5", | ||||||
|                 isMale = true, |                 isMale = true, | ||||||
|                 imgUrl = "aa" |                 imgUrl = "" | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         mBinding.soundActorSelector.setItems(items) |         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) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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<ChatBubble>) { | ||||||
|  |         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) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -6,36 +6,74 @@ import android.view.LayoutInflater | ||||||
| import android.widget.LinearLayout | import android.widget.LinearLayout | ||||||
| import androidx.recyclerview.widget.RecyclerView | import androidx.recyclerview.widget.RecyclerView | ||||||
| import com.drake.brv.annotaion.DividerOrientation | import com.drake.brv.annotaion.DividerOrientation | ||||||
|  | import com.drake.brv.utils.bindingAdapter | ||||||
| import com.drake.brv.utils.divider | import com.drake.brv.utils.divider | ||||||
| import com.drake.brv.utils.grid | import com.drake.brv.utils.grid | ||||||
|  | import com.drake.brv.utils.models | ||||||
| import com.drake.brv.utils.setup | 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.databinding.LayoutSettingBubbleSubViewBinding | ||||||
|  | import com.remax.visualnovel.entity.response.ChatBubble | ||||||
|  | import com.remax.visualnovel.extension.glide.load | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class ExpandBubbleSubView @JvmOverloads constructor( | class ExpandBubbleSubView @JvmOverloads constructor( | ||||||
|     context: Context, |     context: Context, | ||||||
|     attrs: AttributeSet? = null, |     attrs: AttributeSet? = null, | ||||||
|     defStyleAttr: Int = 0 |     defStyleAttr: Int = 0 | ||||||
| ) : LinearLayout(context, attrs, defStyleAttr) { | ) : LinearLayout(context, attrs, defStyleAttr) { | ||||||
| 
 |     private lateinit var items: List<ChatBubble> | ||||||
|     private var mBinding: LayoutSettingBubbleSubViewBinding |     private var mBinding: LayoutSettingBubbleSubViewBinding | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     init { |     init { | ||||||
|         mBinding = LayoutSettingBubbleSubViewBinding.inflate(LayoutInflater.from(context)) |         mBinding = LayoutSettingBubbleSubViewBinding.inflate(LayoutInflater.from(context), this, true) | ||||||
|         with(mBinding) { |         with(mBinding) { | ||||||
|             initRv(itemsRv) |             initRv(itemsRv) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     fun setSelectedSound() { | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private fun initRv(itemsRv: RecyclerView) { |     private fun initRv(itemsRv: RecyclerView) { | ||||||
|         itemsRv.grid(2) |         itemsRv.grid(3) | ||||||
|             .divider { |             .divider { | ||||||
|                 setDivider(16, true) |                 setDivider(16, true) | ||||||
|                 orientation = DividerOrientation.VERTICAL |                 orientation = DividerOrientation.VERTICAL | ||||||
|             }.setup { |             }.setup { | ||||||
|  |                 addType<ChatBubble>(R.layout.layout_item_setting_bubble) | ||||||
| 
 | 
 | ||||||
|  |                 onClick(R.id.tv_select) { | ||||||
|  |                     val bubble = getModel<ChatBubble>() | ||||||
|  |                     if (!bubble.select) { | ||||||
|  |                         itemsRv.bindingAdapter.models?.filterIsInstance<ChatBubble>()?.forEach { item -> | ||||||
|  |                             item.select = item == bubble | ||||||
|  |                         } | ||||||
|  |                         itemsRv.bindingAdapter.notifyDataSetChanged() | ||||||
|  |                         setSelectedSound() | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 onBind { | ||||||
|  |                     val item = getModel<ChatBubble>() | ||||||
|  |                     with(getBinding<LayoutItemSettingBubbleBinding>()) { | ||||||
|  |                         if (!item.imgUrl.isNullOrEmpty()) { | ||||||
|  |                             ivBubble.load(item.imgUrl) | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         ivBubbleName.text = item.name | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     fun setItems(newItems: List<ChatBubble>) { | ||||||
|  |         items = newItems | ||||||
|  |         mBinding.itemsRv.models = items | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | @ -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<ChatMode> = 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<ChatMode>) { | ||||||
|  |         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) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -86,7 +86,7 @@ class ExpandSelectView @JvmOverloads constructor( | ||||||
| 
 | 
 | ||||||
|     private fun createItemView(item: SelectorItem, position: Int): View { |     private fun createItemView(item: SelectorItem, position: Int): View { | ||||||
|         val itemView = LayoutInflater.from(context) |         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<View>(R.id.colorIndicator) |         val colorIndicator = itemView.findViewById<View>(R.id.colorIndicator) | ||||||
|         val itemName = itemView.findViewById<TextView>(R.id.itemName) |         val itemName = itemView.findViewById<TextView>(R.id.itemName) | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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; | ||||||
|  | } | ||||||
|  | @ -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; | ||||||
|  | } | ||||||
|  | @ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -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); | ||||||
|  | } | ||||||
|  | @ -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) { | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -222,6 +222,18 @@ | ||||||
|                 android:layout_height="wrap_content" |                 android:layout_height="wrap_content" | ||||||
|                 /> |                 /> | ||||||
| 
 | 
 | ||||||
|  |             <com.remax.visualnovel.ui.chat.ui.expandableSelector.ExpandChatModeSelectView | ||||||
|  |                 android:id="@+id/chat_model_selector" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 /> | ||||||
|  | 
 | ||||||
|  |             <com.remax.visualnovel.ui.chat.ui.expandableSelector.ExpandBubbleSelectView | ||||||
|  |                 android:id="@+id/bubble_select_view" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 /> | ||||||
|  | 
 | ||||||
|         </com.remax.visualnovel.widget.uitoken.view.UITokenLinearLayout> |         </com.remax.visualnovel.widget.uitoken.view.UITokenLinearLayout> | ||||||
| 
 | 
 | ||||||
|         <!--  background related  --> |         <!--  background related  --> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,63 @@ | ||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <com.remax.visualnovel.widget.uitoken.view.UITokenRelativeLayout | ||||||
|  |     xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="wrap_content" | ||||||
|  |     android:padding="@dimen/dp_12" > | ||||||
|  | 
 | ||||||
|  |     <View | ||||||
|  |         android:id="@+id/selectedDot" | ||||||
|  |         android:layout_width="@dimen/dp_13" | ||||||
|  |         android:layout_height="@dimen/dp_13" | ||||||
|  |         android:layout_alignParentEnd="true" | ||||||
|  |         android:layout_centerVertical="true" | ||||||
|  |         android:background="@drawable/circle_selected" | ||||||
|  |         android:visibility="visible"/> | ||||||
|  | 
 | ||||||
|  |     <LinearLayout | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_toRightOf="@+id/colorIndicator" | ||||||
|  |         android:layout_toLeftOf="@+id/selectedDot" | ||||||
|  |         android:orientation="vertical" | ||||||
|  |         android:layout_marginHorizontal="@dimen/dp_7" | ||||||
|  |         android:layout_centerVertical="true" | ||||||
|  |         > | ||||||
|  | 
 | ||||||
|  |         <com.remax.visualnovel.widget.uitoken.view.UITokenLinearLayout | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:orientation="horizontal" | ||||||
|  |             android:gravity="center_vertical" > | ||||||
|  |             <com.remax.visualnovel.widget.uitoken.view.UITokenTextView | ||||||
|  |                 android:id="@+id/itemName" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:maxLines="1" | ||||||
|  |                 android:textSize="@dimen/sp_13" | ||||||
|  |                 android:text="sssssssss" | ||||||
|  |                 android:textColor="@color/gray3"/> | ||||||
|  | 
 | ||||||
|  |             <com.remax.visualnovel.widget.uitoken.view.UITokenImageView | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:src="@mipmap/chat_vip_str" | ||||||
|  |                 android:layout_marginLeft="@dimen/dp_5" | ||||||
|  |                 /> | ||||||
|  |         </com.remax.visualnovel.widget.uitoken.view.UITokenLinearLayout> | ||||||
|  | 
 | ||||||
|  |         <com.remax.visualnovel.widget.uitoken.view.UITokenTextView | ||||||
|  |             android:id="@+id/mode_description" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:maxLines="1" | ||||||
|  |             android:textSize="@dimen/sp_12" | ||||||
|  |             android:text="aaaaaa" | ||||||
|  |             android:textStyle="italic" | ||||||
|  |             android:textColor="@color/chat_setting_ai_model_des_color"/> | ||||||
|  | 
 | ||||||
|  |     </LinearLayout> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | </com.remax.visualnovel.widget.uitoken.view.UITokenRelativeLayout> | ||||||
|  | @ -15,46 +15,12 @@ | ||||||
|         app:layout_constraintDimensionRatio="h,164:120" |         app:layout_constraintDimensionRatio="h,164:120" | ||||||
|         app:layout_constraintTop_toTopOf="parent" |         app:layout_constraintTop_toTopOf="parent" | ||||||
|         app:radiusToken="@string/radius_l" |         app:radiusToken="@string/radius_l" | ||||||
|         app:strokeColorToken="@string/color_primary_normal" |         app:strokeColorToken="@string/color_txt_tertiary_normal" | ||||||
|         app:strokeWidthToken="@string/border_m" |         app:strokeWidthToken="@string/border_l" | ||||||
|         /> |         /> | ||||||
| 
 | 
 | ||||||
|     <com.remax.visualnovel.widget.uitoken.view.UITokenTextView |     <com.remax.visualnovel.widget.uitoken.view.UITokenTextView | ||||||
|         android:id="@+id/bubbleIcon" |         android:id="@+id/iv_bubble_name" | ||||||
|         android:layout_width="60dp" |  | ||||||
|         android:layout_height="wrap_content" |  | ||||||
|         android:gravity="center" |  | ||||||
|         app:layout_constraintBottom_toBottomOf="@+id/selectBg" |  | ||||||
|         app:layout_constraintEnd_toEndOf="@+id/selectBg" |  | ||||||
|         app:layout_constraintStart_toStartOf="@+id/selectBg" |  | ||||||
|         app:layout_constraintTop_toTopOf="@+id/selectBg" |  | ||||||
|         app:textColorToken="@string/color_txt_primary_normal" |  | ||||||
|         android:text="@string/hi" |  | ||||||
|         app:textToken="@string/txt_body_m" /> |  | ||||||
| 
 |  | ||||||
|     <com.remax.visualnovel.widget.ui.lock.LockTagView |  | ||||||
|         android:id="@+id/lockView" |  | ||||||
|         android:layout_width="wrap_content" |  | ||||||
|         android:layout_height="wrap_content" |  | ||||||
|         android:layout_margin="8dp" |  | ||||||
|         app:layout_constraintEnd_toEndOf="@+id/selectBg" |  | ||||||
|         app:layout_constraintTop_toTopOf="@+id/selectBg" |  | ||||||
|         app:lockTagLabel="privateLabel" /> |  | ||||||
| 
 |  | ||||||
|     <androidx.appcompat.widget.AppCompatImageView |  | ||||||
|         android:id="@+id/bubbleLikeIcon" |  | ||||||
|         android:layout_width="12dp" |  | ||||||
|         android:layout_height="12dp" |  | ||||||
|         android:layout_marginEnd="2dp" |  | ||||||
|         android:src="@mipmap/icon_checked" |  | ||||||
|         app:layout_constraintBottom_toBottomOf="@+id/bubbleName" |  | ||||||
|         app:layout_constraintEnd_toStartOf="@+id/bubbleName" |  | ||||||
|         app:layout_constraintHorizontal_chainStyle="packed" |  | ||||||
|         app:layout_constraintStart_toStartOf="parent" |  | ||||||
|         app:layout_constraintTop_toTopOf="@+id/bubbleName" /> |  | ||||||
| 
 |  | ||||||
|     <com.remax.visualnovel.widget.uitoken.view.UITokenTextView |  | ||||||
|         android:id="@+id/bubbleName" |  | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:layout_marginStart="2dp" |         android:layout_marginStart="2dp" | ||||||
|  | @ -62,31 +28,59 @@ | ||||||
|         android:gravity="top" |         android:gravity="top" | ||||||
|         android:text="@string/default_txt" |         android:text="@string/default_txt" | ||||||
|         app:layout_constraintEnd_toEndOf="parent" |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|         app:layout_constraintStart_toEndOf="@+id/bubbleLikeIcon" |         app:layout_constraintStart_toStartOf="parent" | ||||||
|         app:layout_constraintTop_toBottomOf="@+id/selectBg" |         app:layout_constraintTop_toBottomOf="@+id/selectBg" | ||||||
|         app:textColorToken="@string/color_txt_primary_normal" |         app:textColorToken="@string/color_txt_primary_normal" | ||||||
|         app:textToken="@string/txt_label_m" /> |         app:textToken="@string/txt_label_m" /> | ||||||
| 
 | 
 | ||||||
|     <com.remax.visualnovel.widget.ui.RadioCheckButton | 
 | ||||||
|         android:id="@+id/bubbleCheckBox" |     <com.remax.visualnovel.widget.uitoken.view.UITokenTextView | ||||||
|  |         android:id="@+id/iv_lock_hint" | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:layout_marginTop="8dp" |         android:layout_marginStart="2dp" | ||||||
|         android:layout_marginEnd="8dp" |         android:layout_marginTop="@dimen/dp_10" | ||||||
|  |         android:gravity="top" | ||||||
|  |         android:text="@string/unlocked" | ||||||
|  |         android:textSize="@dimen/sp_12" | ||||||
|  |         android:textStyle="italic" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" | ||||||
|  |         /> | ||||||
|  | 
 | ||||||
|  |     <com.remax.visualnovel.widget.uitoken.view.UITokenImageView | ||||||
|  |         android:id="@+id/iv_left_top" | ||||||
|  |         android:layout_width="@dimen/dp_25" | ||||||
|  |         android:layout_height="@dimen/dp_25" | ||||||
|  |         android:src="@mipmap/chat_vip_str" | ||||||
|  |         android:layout_marginStart="@dimen/dp_15" | ||||||
|  |         android:layout_marginTop="@dimen/dp_5" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" | ||||||
|  |         /> | ||||||
|  | 
 | ||||||
|  |     <com.remax.visualnovel.widget.uitoken.view.UITokenImageView | ||||||
|  |         android:id="@+id/iv_lock_indicator" | ||||||
|  |         android:layout_width="@dimen/dp_25" | ||||||
|  |         android:layout_height="@dimen/dp_25" | ||||||
|  |         android:src="@mipmap/chat_vip_str" | ||||||
|  |         android:layout_marginEnd="@dimen/dp_15" | ||||||
|  |         android:layout_marginTop="@dimen/dp_5" | ||||||
|         app:layout_constraintEnd_toEndOf="parent" |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|         app:layout_constraintTop_toTopOf="parent" |         app:layout_constraintTop_toTopOf="parent" | ||||||
|         app:radioCheck="true" /> |         /> | ||||||
| 
 | 
 | ||||||
|     <!--<com.remax.visualnovel.widget.TagIconView |     <com.remax.visualnovel.widget.uitoken.view.UITokenImageView | ||||||
|         android:id="@+id/bubbleTag" |         android:id="@+id/iv_bubble" | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="wrap_content" | ||||||
|         app:tagIconSize="SIZE_S" |  | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:layout_margin="8dp" |         android:src="@mipmap/chat_left_bg" | ||||||
|         app:layout_constraintStart_toStartOf="@+id/selectBg" |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|         app:layout_constraintTop_toTopOf="@+id/selectBg" |         app:layout_constraintTop_toTopOf="parent" | ||||||
|         app:tagIconContent="@string/default_txt" |         app:layout_constraintStart_toStartOf="parent" | ||||||
|         app:tagIconType="ELEMENT_DARK" />--> |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         /> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| </androidx.constraintlayout.widget.ConstraintLayout> | </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
|  | @ -0,0 +1,73 @@ | ||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <LinearLayout | ||||||
|  |     xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     android:layout_width="wrap_content" | ||||||
|  |     android:layout_height="wrap_content" | ||||||
|  |     android:orientation="vertical"> | ||||||
|  | 
 | ||||||
|  |     <com.remax.visualnovel.widget.RoundFrameLayout | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         app:radius="4dp"> | ||||||
|  | 
 | ||||||
|  |         <androidx.constraintlayout.widget.ConstraintLayout | ||||||
|  |             android:id="@+id/group" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:gravity="center_vertical" | ||||||
|  |             android:orientation="horizontal" | ||||||
|  |             > | ||||||
|  |             <com.remax.visualnovel.widget.RoundFrameLayout | ||||||
|  |                 android:layout_width="0dp" | ||||||
|  |                 android:layout_height="0dp" | ||||||
|  |                 app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |                 app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |                 app:layout_constraintStart_toStartOf="parent" | ||||||
|  |                 app:layout_constraintTop_toTopOf="parent" | ||||||
|  |                 app:radius="4dp"> | ||||||
|  | 
 | ||||||
|  |                 <com.remax.visualnovel.widget.blurview.ShapeBlurView | ||||||
|  |                     android:id="@+id/blurView" | ||||||
|  |                     android:layout_width="match_parent" | ||||||
|  |                     android:layout_height="match_parent" /> | ||||||
|  | 
 | ||||||
|  |             </com.remax.visualnovel.widget.RoundFrameLayout> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             <com.remax.visualnovel.widget.uitoken.view.UITokenTextView | ||||||
|  |                 android:id="@+id/iconView" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_marginStart="8dp" | ||||||
|  |                 android:gravity="center" | ||||||
|  |                 android:text="@string/icon_chat" | ||||||
|  |                 android:textSize="16sp" | ||||||
|  |                 app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |                 app:layout_constraintEnd_toStartOf="@+id/textView" | ||||||
|  |                 app:layout_constraintStart_toStartOf="parent" | ||||||
|  |                 app:layout_constraintTop_toTopOf="parent" | ||||||
|  |                 app:onlyIconFont="true" | ||||||
|  |                 app:textColorToken="@string/color_txt_primary_normal" | ||||||
|  |                 /> | ||||||
|  | 
 | ||||||
|  |             <com.remax.visualnovel.widget.uitoken.view.UITokenTextView | ||||||
|  |                 android:id="@+id/textView" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_marginHorizontal="8dp" | ||||||
|  |                 android:text="@string/tag" | ||||||
|  |                 android:clipChildren="false" | ||||||
|  |                 android:clipToPadding="false" | ||||||
|  |                 app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |                 app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |                 app:layout_constraintStart_toEndOf="@+id/iconView" | ||||||
|  |                 app:layout_constraintTop_toTopOf="parent" | ||||||
|  |                 app:textColorToken="@string/color_txt_primary_normal" | ||||||
|  |                 app:textToken="@string/txt_label_m" /> | ||||||
|  | 
 | ||||||
|  |         </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
|  |     </com.remax.visualnovel.widget.RoundFrameLayout> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | </LinearLayout> | ||||||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.1 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.4 KiB | 
|  | @ -389,7 +389,6 @@ | ||||||
|     <string name="hi">Hi</string> |     <string name="hi">Hi</string> | ||||||
|     <string name="coin_to_create">%d to Create</string> |     <string name="coin_to_create">%d to Create</string> | ||||||
|     <string name="create_image">Create image</string> |     <string name="create_image">Create image</string> | ||||||
|     <string name="unlocked">Unlocked</string> |  | ||||||
|     <string name="waiting_to_be_connected">Waiting to be connected</string> |     <string name="waiting_to_be_connected">Waiting to be connected</string> | ||||||
|     <string name="interrupt">Interrupt</string> |     <string name="interrupt">Interrupt</string> | ||||||
|     <string name="listening">Listening</string> |     <string name="listening">Listening</string> | ||||||
|  | @ -481,5 +480,8 @@ | ||||||
|     <string name="setting_max_response_num">Maximum number of response tokens</string> |     <string name="setting_max_response_num">Maximum number of response tokens</string> | ||||||
|     <string name="font_size">Font Size</string> |     <string name="font_size">Font Size</string> | ||||||
|     <string name="title_sound_actor">Voice actor</string> |     <string name="title_sound_actor">Voice actor</string> | ||||||
|  |     <string name="setting_chat_bubble_title">Chat buttle</string> | ||||||
|  |     <string name="unlocked">Unlocked</string> | ||||||
|  |     <string name="chat_mode">Chat Mode</string> | ||||||
| 
 | 
 | ||||||
| </resources> | </resources> | ||||||
		Loading…
	
		Reference in New Issue