1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settingslib.widget 18 19 import android.content.Context 20 import android.graphics.drawable.Drawable 21 import android.text.Spannable 22 import android.text.SpannableString 23 import android.text.TextPaint 24 import android.text.TextUtils 25 import android.text.method.LinkMovementMethod 26 import android.text.style.ClickableSpan 27 import android.text.style.URLSpan 28 import android.util.AttributeSet 29 import android.view.Gravity 30 import android.view.LayoutInflater 31 import android.view.View 32 import android.widget.TextView 33 import androidx.constraintlayout.widget.ConstraintLayout 34 import com.android.settingslib.widget.theme.R 35 import com.google.android.material.button.MaterialButton 36 37 class CollapsableTextView @JvmOverloads constructor( 38 context: Context, 39 attrs: AttributeSet? = null, 40 defStyleAttr: Int = 0 41 ) : ConstraintLayout(context, attrs, defStyleAttr) { 42 43 private var isCollapsable: Boolean = DEFAULT_COLLAPSABLE 44 private var isCollapsed: Boolean = false 45 private var minLines: Int = DEFAULT_MIN_LINES 46 47 private val titleTextView: TextView 48 private val collapseButton: MaterialButton 49 private val collapseButtonResources: CollapseButtonResources 50 private var hyperlinkListener: View.OnClickListener? = null 51 private var learnMoreListener: View.OnClickListener? = null 52 private var learnMoreText: CharSequence? = null 53 private var learnMoreSpan: LearnMoreSpan? = null 54 val learnMoreTextView: LinkableTextView 55 var isLearnMoreEnabled: Boolean = false 56 57 init { 58 LayoutInflater.from(context) 59 .inflate(R.layout.settingslib_expressive_collapsable_textview, this) 60 titleTextView = findViewById(android.R.id.title) 61 collapseButton = findViewById(R.id.collapse_button) 62 learnMoreTextView = findViewById(R.id.settingslib_expressive_learn_more) 63 64 collapseButtonResources = CollapseButtonResources( 65 context.getDrawable(R.drawable.settingslib_expressive_icon_collapse)!!, 66 context.getDrawable(R.drawable.settingslib_expressive_icon_expand)!!, 67 context.getString(R.string.settingslib_expressive_text_collapse), 68 context.getString(R.string.settingslib_expressive_text_expand) 69 ) 70 <lambda>null71 collapseButton.setOnClickListener { 72 isCollapsed = !isCollapsed 73 updateView() 74 } 75 76 initAttributes(context, attrs, defStyleAttr) 77 } 78 initAttributesnull79 private fun initAttributes(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { 80 context.obtainStyledAttributes( 81 attrs, R.styleable.CollapsableTextView, defStyleAttr, 0 82 ).apply { 83 val gravity = getInt(gravityAttr, Gravity.START) 84 when (gravity) { 85 Gravity.CENTER_VERTICAL, Gravity.CENTER, Gravity.CENTER_HORIZONTAL -> { 86 centerHorizontally(titleTextView) 87 centerHorizontally(collapseButton) 88 centerHorizontally(learnMoreTextView) 89 } 90 } 91 isCollapsable = getBoolean(isCollapsableAttr, DEFAULT_COLLAPSABLE) 92 minLines = getInt(minLinesAttr, DEFAULT_MIN_LINES) 93 recycle() 94 } 95 } 96 centerHorizontallynull97 private fun centerHorizontally(view: View) { 98 when (view) { 99 is MaterialButton -> { 100 (view.layoutParams as LayoutParams).apply { 101 startToStart = LayoutParams.PARENT_ID 102 endToEnd = LayoutParams.PARENT_ID 103 } 104 } 105 is TextView -> { 106 view.gravity = Gravity.CENTER 107 } 108 else -> { 109 (view.layoutParams as LayoutParams).apply { 110 startToStart = LayoutParams.PARENT_ID 111 endToEnd = LayoutParams.PARENT_ID 112 } 113 } 114 } 115 } 116 117 /** 118 * Sets the text content of the CollapsableTextView. 119 * @param text The text to display. 120 */ setTextnull121 fun setText(text: String) { 122 titleTextView.text = text 123 } 124 125 /** 126 * Sets whether the text view is collapsable. 127 * @param collapsable True if the text view should be collapsable, false otherwise. 128 */ setCollapsablenull129 fun setCollapsable(collapsable: Boolean) { 130 isCollapsable = collapsable 131 // Make is collapsed when it's collapsable 132 if (isCollapsable) isCollapsed = true 133 updateView() 134 } 135 136 /** 137 * Sets the minimum number of lines to display when collapsed. 138 * @param lines The minimum number of lines. 139 */ setMinLinesnull140 fun setMinLines(lines: Int) { 141 minLines = lines.coerceIn(1, DEFAULT_MAX_LINES) 142 updateView() 143 } 144 145 /** 146 * Sets the action when clicking on the learn more view. 147 * @param listener The click listener for learn more. 148 */ setLearnMoreActionnull149 fun setLearnMoreAction(listener: View.OnClickListener?) { 150 if (learnMoreListener != listener) { 151 learnMoreListener = listener 152 formatLearnMoreText() 153 } 154 } 155 156 /** 157 * Sets the text of learn more view. 158 * @param text The text of learn more. 159 */ setLearnMoreTextnull160 fun setLearnMoreText(text: CharSequence?) { 161 if (!TextUtils.equals(learnMoreText, text)) { 162 learnMoreText = text 163 formatLearnMoreText() 164 } 165 } 166 setHyperlinkListenernull167 fun setHyperlinkListener(listener: View.OnClickListener?) { 168 if (hyperlinkListener != listener) { 169 hyperlinkListener = listener 170 linkifyTitle() 171 } 172 } 173 linkifyTitlenull174 private fun linkifyTitle() { 175 var text = titleTextView.text.toString() 176 val beginIndex = text.indexOf(LINK_BEGIN_MARKER) 177 text = text.replace(LINK_BEGIN_MARKER, "") 178 val endIndex = text.indexOf(LINK_END_MARKER) 179 text = text.replace(LINK_END_MARKER, "") 180 titleTextView.text = text 181 if (beginIndex == -1 || endIndex == -1 || beginIndex >= endIndex) { 182 return 183 } 184 185 titleTextView.setText(text, TextView.BufferType.SPANNABLE) 186 titleTextView.movementMethod = LinkMovementMethod.getInstance() 187 val spannableContent = titleTextView.getText() as Spannable 188 val spannableLink = object : ClickableSpan() { 189 override fun onClick(widget: View) { 190 hyperlinkListener?.onClick(widget) 191 } 192 193 override fun updateDrawState(ds: TextPaint) { 194 super.updateDrawState(ds) 195 ds.isUnderlineText = true 196 } 197 } 198 spannableContent.setSpan( 199 spannableLink, 200 beginIndex, 201 endIndex, 202 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE 203 ) 204 } 205 formatLearnMoreTextnull206 private fun formatLearnMoreText() { 207 if (learnMoreListener == null || TextUtils.isEmpty(learnMoreText)) { 208 learnMoreTextView.visibility = GONE 209 isLearnMoreEnabled = false 210 return 211 } 212 val spannableLearnMoreText = SpannableString(learnMoreText) 213 if (learnMoreSpan != null) { 214 spannableLearnMoreText.removeSpan(learnMoreSpan) 215 } 216 learnMoreSpan = LearnMoreSpan(clickListener = learnMoreListener!!) 217 spannableLearnMoreText.setSpan(learnMoreSpan, 0, learnMoreText!!.length, 0) 218 learnMoreTextView.text = spannableLearnMoreText 219 learnMoreTextView.visibility = VISIBLE 220 isLearnMoreEnabled = true 221 } 222 updateViewnull223 private fun updateView() { 224 when { 225 isCollapsed -> { 226 collapseButton.apply { 227 text = collapseButtonResources.expandText 228 icon = collapseButtonResources.expandIcon 229 } 230 titleTextView.maxLines = minLines 231 titleTextView.ellipsize = null 232 titleTextView.scrollBarSize = 0 233 } 234 235 else -> { 236 collapseButton.apply { 237 text = collapseButtonResources.collapseText 238 icon = collapseButtonResources.collapseIcon 239 } 240 titleTextView.maxLines = DEFAULT_MAX_LINES 241 titleTextView.ellipsize = TextUtils.TruncateAt.END 242 } 243 } 244 collapseButton.visibility = if (isCollapsable) VISIBLE else GONE 245 learnMoreTextView.visibility = if (isLearnMoreEnabled && !isCollapsed) VISIBLE else GONE 246 } 247 248 private data class CollapseButtonResources( 249 val collapseIcon: Drawable, 250 val expandIcon: Drawable, 251 val collapseText: String, 252 val expandText: String 253 ) 254 255 companion object { 256 private const val DEFAULT_MAX_LINES = 10 257 private const val DEFAULT_MIN_LINES = 2 258 private const val DEFAULT_COLLAPSABLE = true 259 260 private const val LINK_BEGIN_MARKER = "LINK_BEGIN" 261 private const val LINK_END_MARKER = "LINK_END" 262 263 private val gravityAttr = R.styleable.CollapsableTextView_android_gravity 264 private val minLinesAttr = R.styleable.CollapsableTextView_android_minLines 265 private val isCollapsableAttr = R.styleable.CollapsableTextView_isCollapsable 266 } 267 } 268 269 internal class LearnMoreSpan( 270 url: String = "", 271 val clickListener: View.OnClickListener) : URLSpan(url) { onClicknull272 override fun onClick(widget: View) { 273 clickListener.onClick(widget) 274 } 275 } 276