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