1 /* 2 * Copyright (C) 2023 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.systemui.common.ui.view; 18 19 import android.annotation.IntDef; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.content.res.TypedArray; 23 import android.util.AttributeSet; 24 import android.view.LayoutInflater; 25 import android.view.View; 26 import android.view.ViewGroup; 27 import android.widget.ImageView; 28 import android.widget.LinearLayout; 29 import android.widget.SeekBar; 30 31 import com.android.internal.annotations.VisibleForTesting; 32 import com.android.systemui.res.R; 33 34 import java.lang.annotation.Retention; 35 import java.lang.annotation.RetentionPolicy; 36 37 /** 38 * The layout contains a seekbar whose progress could be modified 39 * through the icons on two ends of the seekbar. 40 */ 41 public class SeekBarWithIconButtonsView extends LinearLayout { 42 43 private static final int DEFAULT_SEEKBAR_MAX = 6; 44 private static final int DEFAULT_SEEKBAR_PROGRESS = 0; 45 private static final int DEFAULT_SEEKBAR_TICK_MARK = 0; 46 47 private ViewGroup mIconStartFrame; 48 private ViewGroup mIconEndFrame; 49 private ImageView mIconStart; 50 private ImageView mIconEnd; 51 private SeekBar mSeekbar; 52 private int mSeekBarChangeMagnitude = 1; 53 54 private boolean mSetProgressFromButtonFlag = false; 55 56 private SeekBarChangeListener mSeekBarListener = new SeekBarChangeListener(); 57 private String[] mStateLabels = null; 58 SeekBarWithIconButtonsView(Context context)59 public SeekBarWithIconButtonsView(Context context) { 60 this(context, null); 61 } 62 SeekBarWithIconButtonsView(Context context, AttributeSet attrs)63 public SeekBarWithIconButtonsView(Context context, AttributeSet attrs) { 64 this(context, attrs, 0); 65 } 66 SeekBarWithIconButtonsView(Context context, AttributeSet attrs, int defStyleAttr)67 public SeekBarWithIconButtonsView(Context context, AttributeSet attrs, int defStyleAttr) { 68 this(context, attrs, defStyleAttr, 0); 69 } 70 SeekBarWithIconButtonsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)71 public SeekBarWithIconButtonsView(Context context, 72 AttributeSet attrs, int defStyleAttr, int defStyleRes) { 73 super(context, attrs, defStyleAttr, defStyleRes); 74 75 LayoutInflater.from(context).inflate( 76 R.layout.seekbar_with_icon_buttons, this, /* attachToRoot= */ true); 77 78 mIconStartFrame = findViewById(R.id.icon_start_frame); 79 mIconEndFrame = findViewById(R.id.icon_end_frame); 80 mIconStart = findViewById(R.id.icon_start); 81 mIconEnd = findViewById(R.id.icon_end); 82 mSeekbar = findViewById(R.id.seekbar); 83 84 if (attrs != null) { 85 TypedArray typedArray = context.obtainStyledAttributes( 86 attrs, 87 R.styleable.SeekBarWithIconButtonsView_Layout, 88 defStyleAttr, defStyleRes 89 ); 90 int max = typedArray.getInt( 91 R.styleable.SeekBarWithIconButtonsView_Layout_max, DEFAULT_SEEKBAR_MAX); 92 int progress = typedArray.getInt( 93 R.styleable.SeekBarWithIconButtonsView_Layout_progress, 94 DEFAULT_SEEKBAR_PROGRESS); 95 mSeekbar.setMax(max); 96 setProgress(progress); 97 98 int iconStartFrameContentDescriptionId = typedArray.getResourceId( 99 R.styleable.SeekBarWithIconButtonsView_Layout_iconStartContentDescription, 100 /* defValue= */ 0); 101 int iconEndFrameContentDescriptionId = typedArray.getResourceId( 102 R.styleable.SeekBarWithIconButtonsView_Layout_iconEndContentDescription, 103 /* defValue= */ 0); 104 if (iconStartFrameContentDescriptionId != 0) { 105 final String contentDescription = 106 context.getString(iconStartFrameContentDescriptionId); 107 mIconStartFrame.setContentDescription(contentDescription); 108 } 109 if (iconEndFrameContentDescriptionId != 0) { 110 final String contentDescription = 111 context.getString(iconEndFrameContentDescriptionId); 112 mIconEndFrame.setContentDescription(contentDescription); 113 } 114 int tickMarkId = typedArray.getResourceId( 115 R.styleable.SeekBarWithIconButtonsView_Layout_tickMark, 116 DEFAULT_SEEKBAR_TICK_MARK); 117 if (tickMarkId != DEFAULT_SEEKBAR_TICK_MARK) { 118 mSeekbar.setTickMark(getResources().getDrawable(tickMarkId)); 119 } 120 mSeekBarChangeMagnitude = typedArray.getInt( 121 R.styleable.SeekBarWithIconButtonsView_Layout_seekBarChangeMagnitude, 122 /* defValue= */ 1); 123 } else { 124 mSeekbar.setMax(DEFAULT_SEEKBAR_MAX); 125 setProgress(DEFAULT_SEEKBAR_PROGRESS); 126 } 127 128 mSeekbar.setOnSeekBarChangeListener(mSeekBarListener); 129 130 mIconStartFrame.setOnClickListener((view) -> onIconStartClicked()); 131 mIconEndFrame.setOnClickListener((view) -> onIconEndClicked()); 132 } 133 setIconViewAndFrameEnabled(View iconView, boolean enabled)134 private static void setIconViewAndFrameEnabled(View iconView, boolean enabled) { 135 iconView.setEnabled(enabled); 136 final ViewGroup iconFrame = (ViewGroup) iconView.getParent(); 137 iconFrame.setEnabled(enabled); 138 } 139 140 /** 141 * Stores the String array we would like to use for describing the state of seekbar progress 142 * and updates the state description with current progress. 143 * 144 * @param labels The state descriptions to be announced for each progress. 145 */ setProgressStateLabels(String[] labels)146 public void setProgressStateLabels(String[] labels) { 147 mStateLabels = labels; 148 if (mStateLabels != null) { 149 setSeekbarStateDescription(); 150 } 151 } 152 153 /** 154 * Sets the state of seekbar based on current progress. The progress of seekbar is 155 * corresponding to the index of the string array. If the progress is larger than or equals 156 * to the length of the array, the state description is set to an empty string. 157 */ setSeekbarStateDescription()158 private void setSeekbarStateDescription() { 159 mSeekbar.setStateDescription( 160 (mSeekbar.getProgress() < mStateLabels.length) 161 ? mStateLabels[mSeekbar.getProgress()] : ""); 162 } 163 164 /** 165 * Sets a onSeekbarChangeListener to the seekbar in the layout. 166 * We update the Start Icon and End Icon if needed when the seekbar progress is changed. 167 */ 168 public void setOnSeekBarWithIconButtonsChangeListener( 169 @Nullable OnSeekBarWithIconButtonsChangeListener onSeekBarChangeListener) { 170 mSeekBarListener.setOnSeekBarWithIconButtonsChangeListener(onSeekBarChangeListener); 171 } 172 173 /** 174 * Only for testing. Get previous set mOnSeekBarChangeListener to the seekbar. 175 */ 176 @VisibleForTesting 177 public OnSeekBarWithIconButtonsChangeListener getOnSeekBarWithIconButtonsChangeListener() { 178 return mSeekBarListener.mOnSeekBarChangeListener; 179 } 180 181 /** 182 * Only for testing. Get {@link #mSeekbar} in the layout. 183 */ 184 @VisibleForTesting 185 public SeekBar getSeekbar() { 186 return mSeekbar; 187 } 188 189 /** 190 * Start and End icons might need to be updated when there is a change in seekbar progress. 191 * Icon Start will need to be enabled when the seekbar progress is larger than 0. 192 * Icon End will need to be enabled when the seekbar progress is less than Max. 193 */ 194 private void updateIconViewIfNeeded(int progress) { 195 setIconViewAndFrameEnabled(mIconStart, progress > 0); 196 setIconViewAndFrameEnabled(mIconEnd, progress < mSeekbar.getMax()); 197 } 198 199 /** 200 * Sets max to the seekbar in the layout. 201 */ setMax(int max)202 public void setMax(int max) { 203 mSeekbar.setMax(max); 204 } 205 206 /** 207 * Gets max to the seekbar in the layout. 208 */ getMax()209 public int getMax() { 210 return mSeekbar.getMax(); 211 } 212 213 /** 214 * @return the magnitude by which seekbar progress changes when start and end icons are clicked. 215 */ getChangeMagnitude()216 public int getChangeMagnitude() { 217 return mSeekBarChangeMagnitude; 218 } 219 220 /** 221 * Sets progress to the seekbar in the layout. 222 * If the progress is smaller than or equals to 0, the IconStart will be disabled. If the 223 * progress is larger than or equals to Max, the IconEnd will be disabled. The seekbar progress 224 * will be constrained in {@link SeekBar}. 225 */ setProgress(int progress)226 public void setProgress(int progress) { 227 mSeekbar.setProgress(progress); 228 updateIconViewIfNeeded(mSeekbar.getProgress()); 229 } 230 setProgressFromButton(int progress)231 private void setProgressFromButton(int progress) { 232 mSetProgressFromButtonFlag = true; 233 mSeekbar.setProgress(progress); 234 updateIconViewIfNeeded(mSeekbar.getProgress()); 235 } 236 onIconStartClicked()237 private void onIconStartClicked() { 238 final int progress = mSeekbar.getProgress(); 239 if (progress > 0) { 240 setProgressFromButton(progress - mSeekBarChangeMagnitude); 241 } 242 } 243 onIconEndClicked()244 private void onIconEndClicked() { 245 final int progress = mSeekbar.getProgress(); 246 if (progress < mSeekbar.getMax()) { 247 setProgressFromButton(progress + mSeekBarChangeMagnitude); 248 } 249 } 250 251 /** 252 * Get current seekbar progress 253 * 254 * @return 255 */ 256 @VisibleForTesting getProgress()257 public int getProgress() { 258 return mSeekbar.getProgress(); 259 } 260 261 /** 262 * Extended from {@link SeekBar.OnSeekBarChangeListener} to add callback to notify the listeners 263 * the user interaction with the SeekBarWithIconButtonsView is finalized. 264 */ 265 public interface OnSeekBarWithIconButtonsChangeListener 266 extends SeekBar.OnSeekBarChangeListener { 267 268 @Retention(RetentionPolicy.SOURCE) 269 @IntDef({ 270 ControlUnitType.SLIDER, 271 ControlUnitType.BUTTON 272 }) 273 /** Denotes the Last user interacted control unit type. */ 274 @interface ControlUnitType { 275 int SLIDER = 0; 276 int BUTTON = 1; 277 } 278 279 /** 280 * Notification that the user interaction with SeekBarWithIconButtonsView is finalized. This 281 * would be triggered after user ends dragging on the slider or clicks icon buttons. 282 * 283 * @param seekBar The SeekBar in which the user ends interaction with 284 * @param control The last user interacted control unit. It would be 285 * {@link ControlUnitType#SLIDER} if the user was changing the seekbar 286 * progress through dragging the slider, or {@link ControlUnitType#BUTTON} 287 * is the user was clicking button to change the progress. 288 */ onUserInteractionFinalized(SeekBar seekBar, @ControlUnitType int control)289 void onUserInteractionFinalized(SeekBar seekBar, @ControlUnitType int control); 290 } 291 292 private class SeekBarChangeListener implements SeekBar.OnSeekBarChangeListener { 293 private OnSeekBarWithIconButtonsChangeListener mOnSeekBarChangeListener = null; 294 295 @Override onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)296 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 297 if (mStateLabels != null) { 298 setSeekbarStateDescription(); 299 } 300 if (mOnSeekBarChangeListener != null) { 301 if (mSetProgressFromButtonFlag) { 302 mSetProgressFromButtonFlag = false; 303 mOnSeekBarChangeListener.onProgressChanged( 304 seekBar, progress, /* fromUser= */ true); 305 // Directly trigger onUserInteractionFinalized since the interaction 306 // (click button) is ended. 307 mOnSeekBarChangeListener.onUserInteractionFinalized( 308 seekBar, OnSeekBarWithIconButtonsChangeListener.ControlUnitType.BUTTON); 309 } else { 310 mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser); 311 } 312 } 313 updateIconViewIfNeeded(progress); 314 } 315 316 @Override onStartTrackingTouch(SeekBar seekBar)317 public void onStartTrackingTouch(SeekBar seekBar) { 318 if (mOnSeekBarChangeListener != null) { 319 mOnSeekBarChangeListener.onStartTrackingTouch(seekBar); 320 } 321 } 322 323 @Override onStopTrackingTouch(SeekBar seekBar)324 public void onStopTrackingTouch(SeekBar seekBar) { 325 if (mOnSeekBarChangeListener != null) { 326 mOnSeekBarChangeListener.onStopTrackingTouch(seekBar); 327 mOnSeekBarChangeListener.onUserInteractionFinalized( 328 seekBar, OnSeekBarWithIconButtonsChangeListener.ControlUnitType.SLIDER); 329 } 330 } 331 setOnSeekBarWithIconButtonsChangeListener( OnSeekBarWithIconButtonsChangeListener listener)332 void setOnSeekBarWithIconButtonsChangeListener( 333 OnSeekBarWithIconButtonsChangeListener listener) { 334 mOnSeekBarChangeListener = listener; 335 } 336 } 337 } 338