• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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