1 /*
<lambda>null2  * Copyright (C) 2022 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 package com.android.healthconnect.controller.filters
17 
18 import android.animation.ValueAnimator
19 import android.content.Context
20 import android.graphics.Color
21 import android.graphics.drawable.ColorDrawable
22 import android.graphics.drawable.Drawable
23 import android.graphics.drawable.LayerDrawable
24 import android.graphics.drawable.StateListDrawable
25 import android.util.AttributeSet
26 import android.view.View
27 import android.widget.RadioGroup
28 import androidx.appcompat.content.res.AppCompatResources
29 import androidx.appcompat.widget.AppCompatRadioButton
30 import com.android.healthconnect.controller.R
31 import com.android.healthconnect.controller.utils.AttributeResolver
32 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
33 import com.android.healthconnect.controller.utils.logging.HealthConnectLoggerEntryPoint
34 import com.android.healthconnect.controller.utils.logging.PermissionTypesElement
35 import dagger.hilt.android.EntryPointAccessors
36 
37 /**
38  * The FilterChip is a stylised RadioButton which helps the user filter Health Connect data by the
39  * contributing app.
40  *
41  * Each chip belongs to a RadioGroup and behaves like a RadioButton. There are two states for each
42  * FilterChip:
43  * 1. Selected (checked)
44  * 2. Unselected (unchecked)
45  *
46  * An separate icon can be set for each of the two states. If the `selected` icon is not specified,
47  * a default check icon is used. If the `unselected` icon is not specified, no icon will show when
48  * the chip is unchecked and the left padding is adjusted accordingly. By default, the width changes
49  * of a FilterChip are animated.
50  */
51 class FilterChip
52 @JvmOverloads
53 constructor(
54     context: Context,
55     attrs: AttributeSet? = null,
56     defStyleAttr: Int = R.attr.chipStyle,
57 ) : AppCompatRadioButton(context, attrs, defStyleAttr) {
58 
59     private var logger: HealthConnectLogger
60 
61     init {
62         val hiltEntryPoint =
63             EntryPointAccessors.fromApplication(
64                 context.applicationContext, HealthConnectLoggerEntryPoint::class.java)
65         logger = hiltEntryPoint.logger()
66     }
67 
68     private var selectedIcon: Drawable? = null
69     var unSelectedIcon: Drawable? = null
70 
71     fun setSelectedIcon(res: Drawable?) {
72         selectedIcon = res
73         buttonDrawable = makeSelector()
74     }
75 
76     fun setUnselectedIcon(res: Drawable?) {
77         unSelectedIcon = res
78         buttonDrawable = makeSelector()
79     }
80 
81     private val spacingXSmallPx = (context.resources.getDimension(R.dimen.spacing_xsmall)).toInt()
82     private val spacingSmallPx = (context.resources.getDimension(R.dimen.spacing_small)).toInt()
83     private val spacingNormalPx = (context.resources.getDimension(R.dimen.spacing_normal)).toInt()
84 
85     init {
86 
87         val params =
88             RadioGroup.LayoutParams(
89                 RadioGroup.LayoutParams.WRAP_CONTENT, RadioGroup.LayoutParams.WRAP_CONTENT)
90 
91         val px = (context.resources.getDimension(R.dimen.spacing_small)).toInt()
92         params.setMargins(0, 0, px, 0)
93         this.layoutParams = params
94 
95         if (unSelectedIcon == null) {
96             // Padding needs to be changed programmatically when no button icon is used
97             setChipPadding(this.isChecked)
98         }
99 
100         buttonDrawable = makeSelector()
101     }
102 
103     private fun setChipPadding(isChecked: Boolean) {
104         // Padding needs to be changed programmatically when no button icon is used
105         if (isChecked) {
106             this.setPadding(spacingSmallPx, spacingXSmallPx, spacingNormalPx, spacingXSmallPx)
107         } else {
108             this.setPadding(spacingNormalPx, spacingXSmallPx, spacingNormalPx, spacingXSmallPx)
109         }
110     }
111 
112     override fun onAttachedToWindow() {
113         super.onAttachedToWindow()
114         logger.logImpression(PermissionTypesElement.APP_FILTER_BUTTON)
115 
116         this.setOnCheckedChangeListener { buttonView, isChecked ->
117             if (unSelectedIcon == null) {
118                 setChipPadding(isChecked)
119                 animateLayoutChanges(buttonView)
120             }
121         }
122     }
123 
124     override fun setOnClickListener(l: OnClickListener?) {
125         val loggingClickListener = OnClickListener {
126             logger.logInteraction(PermissionTypesElement.APP_FILTER_BUTTON)
127             l?.onClick(it)
128         }
129         super.setOnClickListener(loggingClickListener)
130     }
131 
132     private fun animateLayoutChanges(view: View) {
133         val oldWidth = view.width
134         view.measure(RadioGroup.LayoutParams.MATCH_PARENT, RadioGroup.LayoutParams.WRAP_CONTENT)
135         val targetWidth = view.measuredWidth
136 
137         val animator = ValueAnimator.ofInt(oldWidth, targetWidth)
138         animator.duration = 200
139         animator.addUpdateListener {
140             view.layoutParams.width = it.animatedValue as Int
141             view.requestLayout()
142         }
143         animator.start()
144     }
145 
146     private fun makeSelector(): StateListDrawable {
147         val res = StateListDrawable()
148         val checkedLayers =
149             AppCompatResources.getDrawable(context, R.drawable.filter_chip_button_icon_layer)
150                 as LayerDrawable
151         val checkedDrawable =
152             selectedIcon ?: AttributeResolver.getDrawable(context, R.attr.checkIcon)
153         checkedLayers.setDrawableByLayerId(R.id.icon_layer, checkedDrawable)
154 
155         res.addState(intArrayOf(android.R.attr.state_checked), checkedLayers)
156 
157         if (unSelectedIcon == null) {
158             res.addState(intArrayOf(), ColorDrawable(Color.TRANSPARENT))
159         } else {
160             val uncheckedLayers =
161                 AppCompatResources.getDrawable(context, R.drawable.filter_chip_button_icon_layer)
162                     as LayerDrawable
163             uncheckedLayers.setDrawableByLayerId(R.id.icon_layer, unSelectedIcon)
164 
165             res.addState(intArrayOf(), uncheckedLayers)
166         }
167 
168         return res
169     }
170 }
171