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