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