chat mode 选择器
This commit is contained in:
		
							parent
							
								
									293405c8d1
								
							
						
					
					
						commit
						7358fbd8e2
					
				|  | @ -6,11 +6,6 @@ import kotlinx.parcelize.Parcelize | |||
| 
 | ||||
| @Parcelize | ||||
| data class ChatBubble( | ||||
|     /** | ||||
|      * code | ||||
|      */ | ||||
|     val code: String, | ||||
| 
 | ||||
|     /** | ||||
|      * id | ||||
|      */ | ||||
|  | @ -40,7 +35,7 @@ data class ChatBubble( | |||
|      * 解锁类型 MEMBER:会员 HEARTBEAT_LEVEL:心动等级 | ||||
|      */ | ||||
|     val unlockType: String? = null, | ||||
|     var isDefault: Boolean, | ||||
|     var isDefault: Boolean = false, | ||||
|     var select: Boolean = false | ||||
| ) : Parcelable { | ||||
|     companion object { | ||||
|  |  | |||
|  | @ -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 com.remax.visualnovel.R | ||||
| import com.remax.visualnovel.databinding.LayoutChatMenuViewBinding | ||||
| import com.remax.visualnovel.entity.response.ChatBubble | ||||
| import com.remax.visualnovel.entity.response.ChatMode | ||||
| import com.remax.visualnovel.entity.response.ChatSound | ||||
| import com.remax.visualnovel.ui.chat.ui.expandableSelector.SelectorItem | ||||
| 
 | ||||
|  | @ -27,7 +29,9 @@ class ChatSettingView @JvmOverloads constructor( | |||
|         } | ||||
| 
 | ||||
|         initAiModelSelectorView() | ||||
|         initChatModeSelectorView() | ||||
|         initSoundSelectorView() | ||||
|         initBubbleSelectView() | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -67,6 +71,34 @@ class ChatSettingView @JvmOverloads constructor( | |||
|         mBinding.aiModelSelector.selectItem(0) | ||||
|     } | ||||
| 
 | ||||
|     fun initChatModeSelectorView() { | ||||
|         val items = listOf( | ||||
|             ChatMode( | ||||
|                 name = "Mode-1", | ||||
|                 description = "Previous-generation large model", | ||||
|             ), | ||||
|             ChatMode( | ||||
|                 name = "Mode-2", | ||||
|                 description = "aaaaaaaaaaaaaaaaaaa", | ||||
|             ), | ||||
|             ChatMode( | ||||
|                 name = "Mode-3", | ||||
|                 description = "ccccccccccccccccccccccccc", | ||||
|             ), | ||||
|             ChatMode( | ||||
|                 name = "Mode-4", | ||||
|                 description = "Pppppppppppppppppppppp", | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|         //aiModelSelector.setOnItemSelectedListener() | ||||
|         mBinding.chatModelSelector.setTitleIcon(R.mipmap.setting_chat_mode_icon) | ||||
|         mBinding.chatModelSelector.setTitleText(R.string.chat_mode) | ||||
|         mBinding.chatModelSelector.setItems(items) | ||||
|         mBinding.chatModelSelector.selectItem(0) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     fun initSoundSelectorView() { | ||||
|         val items = listOf( | ||||
|             ChatSound( | ||||
|  | @ -74,14 +106,14 @@ class ChatSettingView @JvmOverloads constructor( | |||
|                 name = "Sound-1", | ||||
|                 description = "This is description for sound-1", | ||||
|                 isMale = true, | ||||
|                 imgUrl = "aa" | ||||
|                 imgUrl = "" | ||||
|             ), | ||||
|             ChatSound( | ||||
|                 id = 2L, | ||||
|                 name = "Sound-2", | ||||
|                 description = "This is description for sound-2", | ||||
|                 isMale = true, | ||||
|                 imgUrl = "aa" | ||||
|                 imgUrl = "" | ||||
|             ), | ||||
| 
 | ||||
|             ChatSound( | ||||
|  | @ -89,7 +121,7 @@ class ChatSettingView @JvmOverloads constructor( | |||
|                 name = "Sound-3", | ||||
|                 description = "This is description for sound-3", | ||||
|                 isMale = true, | ||||
|                 imgUrl = "aa" | ||||
|                 imgUrl = "" | ||||
|             ), | ||||
| 
 | ||||
|             ChatSound( | ||||
|  | @ -97,7 +129,7 @@ class ChatSettingView @JvmOverloads constructor( | |||
|                 name = "Sound-4", | ||||
|                 description = "This is description for sound-4", | ||||
|                 isMale = true, | ||||
|                 imgUrl = "aa" | ||||
|                 imgUrl = "" | ||||
|             ), | ||||
| 
 | ||||
|             ChatSound( | ||||
|  | @ -105,12 +137,47 @@ class ChatSettingView @JvmOverloads constructor( | |||
|                 name = "Sound-5", | ||||
|                 description = "This is description for sound-5", | ||||
|                 isMale = true, | ||||
|                 imgUrl = "aa" | ||||
|                 imgUrl = "" | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|         mBinding.soundActorSelector.setItems(items) | ||||
|     } | ||||
| 
 | ||||
|     fun initBubbleSelectView() { | ||||
|         val items = listOf( | ||||
|             ChatBubble( | ||||
|                 id = 1L, | ||||
|                 name = "Bubble-1", | ||||
|                 imgUrl = "" | ||||
|             ), | ||||
|             ChatBubble( | ||||
|                 id = 2L, | ||||
|                 name = "Bubble-2", | ||||
|                 imgUrl = "" | ||||
|             ), | ||||
| 
 | ||||
|             ChatBubble( | ||||
|                 id = 3L, | ||||
|                 name = "Bubble-3", | ||||
|                 imgUrl = "" | ||||
|             ), | ||||
| 
 | ||||
|             ChatBubble( | ||||
|                 id = 4L, | ||||
|                 name = "Bubble-4", | ||||
|                 imgUrl = "" | ||||
|             ), | ||||
| 
 | ||||
|             ChatBubble( | ||||
|                 id = 5L, | ||||
|                 name = "Bubble-5", | ||||
|                 imgUrl = "" | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|         mBinding.bubbleSelectView.setItems(items) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -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 androidx.recyclerview.widget.RecyclerView | ||||
| import com.drake.brv.annotaion.DividerOrientation | ||||
| import com.drake.brv.utils.bindingAdapter | ||||
| import com.drake.brv.utils.divider | ||||
| import com.drake.brv.utils.grid | ||||
| import com.drake.brv.utils.models | ||||
| import com.drake.brv.utils.setup | ||||
| import com.remax.visualnovel.R | ||||
| import com.remax.visualnovel.databinding.LayoutItemSettingBubbleBinding | ||||
| import com.remax.visualnovel.databinding.LayoutSettingBubbleSubViewBinding | ||||
| import com.remax.visualnovel.entity.response.ChatBubble | ||||
| import com.remax.visualnovel.extension.glide.load | ||||
| 
 | ||||
| 
 | ||||
| class ExpandBubbleSubView @JvmOverloads constructor( | ||||
|     context: Context, | ||||
|     attrs: AttributeSet? = null, | ||||
|     defStyleAttr: Int = 0 | ||||
| ) : LinearLayout(context, attrs, defStyleAttr) { | ||||
| 
 | ||||
|     private lateinit var items: List<ChatBubble> | ||||
|     private var mBinding: LayoutSettingBubbleSubViewBinding | ||||
| 
 | ||||
| 
 | ||||
|     init { | ||||
|         mBinding = LayoutSettingBubbleSubViewBinding.inflate(LayoutInflater.from(context)) | ||||
|         mBinding = LayoutSettingBubbleSubViewBinding.inflate(LayoutInflater.from(context), this, true) | ||||
|         with(mBinding) { | ||||
|             initRv(itemsRv) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun setSelectedSound() { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private fun initRv(itemsRv: RecyclerView) { | ||||
|         itemsRv.grid(2) | ||||
|         itemsRv.grid(3) | ||||
|             .divider { | ||||
|                 setDivider(16, true) | ||||
|                 orientation = DividerOrientation.VERTICAL | ||||
|             }.setup { | ||||
|                 addType<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 { | ||||
|         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 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" | ||||
|                 /> | ||||
| 
 | ||||
|             <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> | ||||
| 
 | ||||
|         <!--  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_constraintTop_toTopOf="parent" | ||||
|         app:radiusToken="@string/radius_l" | ||||
|         app:strokeColorToken="@string/color_primary_normal" | ||||
|         app:strokeWidthToken="@string/border_m" | ||||
|         app:strokeColorToken="@string/color_txt_tertiary_normal" | ||||
|         app:strokeWidthToken="@string/border_l" | ||||
|         /> | ||||
| 
 | ||||
|     <com.remax.visualnovel.widget.uitoken.view.UITokenTextView | ||||
|         android:id="@+id/bubbleIcon" | ||||
|         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:id="@+id/iv_bubble_name" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginStart="2dp" | ||||
|  | @ -62,31 +28,59 @@ | |||
|         android:gravity="top" | ||||
|         android:text="@string/default_txt" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintStart_toEndOf="@+id/bubbleLikeIcon" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintTop_toBottomOf="@+id/selectBg" | ||||
|         app:textColorToken="@string/color_txt_primary_normal" | ||||
|         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_height="wrap_content" | ||||
|         android:layout_marginTop="8dp" | ||||
|         android:layout_marginEnd="8dp" | ||||
|         android:layout_marginStart="2dp" | ||||
|         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_constraintTop_toTopOf="parent" | ||||
|         app:radioCheck="true" /> | ||||
|         /> | ||||
| 
 | ||||
|     <!--<com.remax.visualnovel.widget.TagIconView | ||||
|         android:id="@+id/bubbleTag" | ||||
|     <com.remax.visualnovel.widget.uitoken.view.UITokenImageView | ||||
|         android:id="@+id/iv_bubble" | ||||
|         android:layout_width="wrap_content" | ||||
|         app:tagIconSize="SIZE_S" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_margin="8dp" | ||||
|         app:layout_constraintStart_toStartOf="@+id/selectBg" | ||||
|         app:layout_constraintTop_toTopOf="@+id/selectBg" | ||||
|         app:tagIconContent="@string/default_txt" | ||||
|         app:tagIconType="ELEMENT_DARK" />--> | ||||
|         android:src="@mipmap/chat_left_bg" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="parent" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         /> | ||||
| 
 | ||||
| 
 | ||||
| </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="coin_to_create">%d to Create</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="interrupt">Interrupt</string> | ||||
|     <string name="listening">Listening</string> | ||||
|  | @ -481,5 +480,8 @@ | |||
|     <string name="setting_max_response_num">Maximum number of response tokens</string> | ||||
|     <string name="font_size">Font Size</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> | ||||
		Loading…
	
		Reference in New Issue