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 = false 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, Attrs, 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 } 89 } 90 recycle() 91 } 92 } 93 centerHorizontallynull94 private fun centerHorizontally(view: View) { 95 (view.layoutParams as LayoutParams).apply { 96 startToStart = LayoutParams.PARENT_ID 97 endToEnd = LayoutParams.PARENT_ID 98 horizontalBias = 0.5f 99 } 100 } 101 102 /** 103 * Sets the text content of the CollapsableTextView. 104 * @param text The text to display. 105 */ setTextnull106 fun setText(text: String) { 107 titleTextView.text = text 108 } 109 110 /** 111 * Sets whether the text view is collapsable. 112 * @param collapsable True if the text view should be collapsable, false otherwise. 113 */ setCollapsablenull114 fun setCollapsable(collapsable: Boolean) { 115 isCollapsable = collapsable 116 updateView() 117 } 118 119 /** 120 * Sets the minimum number of lines to display when collapsed. 121 * @param lines The minimum number of lines. 122 */ setMinLinesnull123 fun setMinLines(line: Int) { 124 minLines = line.coerceIn(1, DEFAULT_MAX_LINES) 125 updateView() 126 } 127 128 /** 129 * Sets the action when clicking on the learn more view. 130 * @param listener The click listener for learn more. 131 */ setLearnMoreActionnull132 fun setLearnMoreAction(listener: View.OnClickListener?) { 133 if (learnMoreListener != listener) { 134 learnMoreListener = listener 135 formatLearnMoreText() 136 } 137 } 138 139 /** 140 * Sets the text of learn more view. 141 * @param text The text of learn more. 142 */ setLearnMoreTextnull143 fun setLearnMoreText(text: CharSequence?) { 144 if (!TextUtils.equals(learnMoreText, text)) { 145 learnMoreText = text 146 formatLearnMoreText() 147 } 148 } 149 setHyperlinkListenernull150 fun setHyperlinkListener(listener: View.OnClickListener?) { 151 if (hyperlinkListener != listener) { 152 hyperlinkListener = listener 153 linkifyTitle() 154 } 155 } 156 linkifyTitlenull157 private fun linkifyTitle() { 158 var text = titleTextView.text.toString() 159 val beginIndex = text.indexOf(LINK_BEGIN_MARKER) 160 text = text.replace(LINK_BEGIN_MARKER, "") 161 val endIndex = text.indexOf(LINK_END_MARKER) 162 text = text.replace(LINK_END_MARKER, "") 163 titleTextView.text = text 164 if (beginIndex == -1 || endIndex == -1 || beginIndex >= endIndex) { 165 return 166 } 167 168 titleTextView.setText(text, TextView.BufferType.SPANNABLE) 169 titleTextView.movementMethod = LinkMovementMethod.getInstance() 170 val spannableContent = titleTextView.getText() as Spannable 171 val spannableLink = object : ClickableSpan() { 172 override fun onClick(widget: View) { 173 hyperlinkListener?.onClick(widget) 174 } 175 176 override fun updateDrawState(ds: TextPaint) { 177 super.updateDrawState(ds) 178 ds.isUnderlineText = true 179 } 180 } 181 spannableContent.setSpan( 182 spannableLink, 183 beginIndex, 184 endIndex, 185 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE 186 ) 187 } 188 formatLearnMoreTextnull189 private fun formatLearnMoreText() { 190 if (learnMoreListener == null || TextUtils.isEmpty(learnMoreText)) { 191 learnMoreTextView.visibility = GONE 192 isLearnMoreEnabled = false 193 return 194 } 195 val spannableLearnMoreText = SpannableString(learnMoreText) 196 if (learnMoreSpan != null) { 197 spannableLearnMoreText.removeSpan(learnMoreSpan) 198 } 199 learnMoreSpan = LearnMoreSpan(clickListener = learnMoreListener!!) 200 spannableLearnMoreText.setSpan(learnMoreSpan, 0, learnMoreText!!.length, 0) 201 learnMoreTextView.setText(spannableLearnMoreText) 202 learnMoreTextView.visibility = VISIBLE 203 isLearnMoreEnabled = true 204 } 205 updateViewnull206 private fun updateView() { 207 when { 208 isCollapsed -> { 209 collapseButton.apply { 210 text = collapseButtonResources.expandText 211 icon = collapseButtonResources.expandIcon 212 } 213 titleTextView.maxLines = minLines 214 } 215 216 else -> { 217 collapseButton.apply { 218 text = collapseButtonResources.collapseText 219 icon = collapseButtonResources.collapseIcon 220 } 221 titleTextView.maxLines = DEFAULT_MAX_LINES 222 } 223 } 224 collapseButton.visibility = if (isCollapsable) VISIBLE else GONE 225 learnMoreTextView.visibility = if (isLearnMoreEnabled && !isCollapsed) VISIBLE else GONE 226 } 227 228 private data class CollapseButtonResources( 229 val collapseIcon: Drawable, 230 val expandIcon: Drawable, 231 val collapseText: String, 232 val expandText: String 233 ) 234 235 companion object { 236 private const val DEFAULT_MAX_LINES = 10 237 private const val DEFAULT_MIN_LINES = 2 238 239 private const val LINK_BEGIN_MARKER = "LINK_BEGIN" 240 private const val LINK_END_MARKER = "LINK_END" 241 242 private val Attrs = R.styleable.CollapsableTextView 243 private val GravityAttr = R.styleable.CollapsableTextView_android_gravity 244 } 245 } 246 247 internal class LearnMoreSpan( 248 val url: String = "", 249 val clickListener: View.OnClickListener) : URLSpan(url) { onClicknull250 override fun onClick(widget: View) { 251 clickListener.onClick(widget) 252 } 253 } 254