• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.settings.accessibility;
18 
19 import static android.view.HapticFeedbackConstants.CLOCK_TICK;
20 
21 import static com.android.settings.Utils.isNightMode;
22 
23 import android.annotation.StringRes;
24 import android.content.Context;
25 import android.content.res.ColorStateList;
26 import android.content.res.Resources;
27 import android.graphics.Canvas;
28 import android.graphics.Color;
29 import android.graphics.Paint;
30 import android.graphics.Rect;
31 import android.os.UserHandle;
32 import android.provider.Settings;
33 import android.util.AttributeSet;
34 import android.widget.SeekBar;
35 
36 import androidx.annotation.VisibleForTesting;
37 
38 import com.android.settings.R;
39 import com.android.settings.Utils;
40 
41 /**
42  * A custom seekbar for the balance setting.
43  *
44  * Adds a center line indicator between left and right, which snaps to if close.
45  * Updates Settings.System for balance on progress changed.
46  */
47 public class BalanceSeekBar extends SeekBar {
48     private final Context mContext;
49     private final Object mListenerLock = new Object();
50     private OnSeekBarChangeListener mOnSeekBarChangeListener;
51     private int mLastProgress = -1;
52     private final OnSeekBarChangeListener mProxySeekBarListener = new OnSeekBarChangeListener() {
53         @Override
54         public void onStopTrackingTouch(SeekBar seekBar) {
55             synchronized (mListenerLock) {
56                 if (mOnSeekBarChangeListener != null) {
57                     mOnSeekBarChangeListener.onStopTrackingTouch(seekBar);
58                 }
59             }
60         }
61 
62         @Override
63         public void onStartTrackingTouch(SeekBar seekBar) {
64             synchronized (mListenerLock) {
65                 if (mOnSeekBarChangeListener != null) {
66                     mOnSeekBarChangeListener.onStartTrackingTouch(seekBar);
67                 }
68             }
69         }
70 
71         @Override
72         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
73             if (fromUser) {
74                 // Snap to centre when within the specified threshold
75                 if (progress != mCenter
76                         && progress > mCenter - mSnapThreshold
77                         && progress < mCenter + mSnapThreshold) {
78                     progress = mCenter;
79                     seekBar.setProgress(progress); // direct update (fromUser becomes false)
80                 }
81                 if (progress != mLastProgress) {
82                     if (progress == mCenter || progress == getMin() || progress == getMax()) {
83                         seekBar.performHapticFeedback(CLOCK_TICK);
84                     }
85                     mLastProgress = progress;
86                 }
87                 final float balance = (progress - mCenter) * 0.01f;
88                 Settings.System.putFloatForUser(mContext.getContentResolver(),
89                         Settings.System.MASTER_BALANCE, balance, UserHandle.USER_CURRENT);
90             }
91             final int max = getMax();
92             if (max > 0) {
93                 seekBar.setStateDescription(createStateDescription(mContext,
94                         R.string.audio_seek_bar_state_left_first,
95                         R.string.audio_seek_bar_state_right_first,
96                         progress,
97                         max));
98             }
99             // If fromUser is false, the call is a set from the framework on creation or on
100             // internal update. The progress may be zero, ignore (don't change system settings).
101 
102             // after adjusting the seekbar, notify downstream listener.
103             // note that progress may have been adjusted in the code above to mCenter.
104             synchronized (mListenerLock) {
105                 if (mOnSeekBarChangeListener != null) {
106                     mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser);
107                 }
108             }
109         }
110     };
111 
112     // Percentage of max to be used as a snap to threshold
113     @VisibleForTesting
114     static final float SNAP_TO_PERCENTAGE = 0.03f;
115     private final Paint mCenterMarkerPaint;
116     private final Rect mCenterMarkerRect;
117     // changed in setMax()
118     private float mSnapThreshold;
119     private int mCenter;
120 
BalanceSeekBar(Context context, AttributeSet attrs)121     public BalanceSeekBar(Context context, AttributeSet attrs) {
122         this(context, attrs, com.android.internal.R.attr.seekBarStyle);
123     }
124 
BalanceSeekBar(Context context, AttributeSet attrs, int defStyleAttr)125     public BalanceSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
126         this(context, attrs, defStyleAttr, 0 /* defStyleRes */);
127     }
128 
BalanceSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)129     public BalanceSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
130         super(context, attrs, defStyleAttr, defStyleRes);
131         mContext = context;
132         Resources res = getResources();
133         mCenterMarkerRect = new Rect(0 /* left */, 0 /* top */,
134                 res.getDimensionPixelSize(R.dimen.balance_seekbar_center_marker_width),
135                 res.getDimensionPixelSize(R.dimen.balance_seekbar_center_marker_height));
136         mCenterMarkerPaint = new Paint();
137         // TODO use a more suitable colour?
138         mCenterMarkerPaint.setColor(isNightMode(context) ? Color.WHITE : Color.BLACK);
139         mCenterMarkerPaint.setStyle(Paint.Style.FILL);
140         // Remove the progress colour
141         setProgressTintList(ColorStateList.valueOf(Color.TRANSPARENT));
142 
143         super.setOnSeekBarChangeListener(mProxySeekBarListener);
144     }
145 
146     @Override
setOnSeekBarChangeListener(OnSeekBarChangeListener listener)147     public void setOnSeekBarChangeListener(OnSeekBarChangeListener listener) {
148         synchronized (mListenerLock) {
149             mOnSeekBarChangeListener = listener;
150         }
151     }
152 
153     // Note: the superclass AbsSeekBar.setMax is synchronized.
154     @Override
setMax(int max)155     public synchronized void setMax(int max) {
156         super.setMax(max);
157         // update snap to threshold
158         mCenter = max / 2;
159         mSnapThreshold = max * SNAP_TO_PERCENTAGE;
160     }
161 
162     // Note: the superclass AbsSeekBar.onDraw is synchronized.
163     @Override
onDraw(Canvas canvas)164     protected synchronized void onDraw(Canvas canvas) {
165         // Draw a vertical line at 50% that represents centred balance
166         int seekBarCenter = (canvas.getHeight() - getPaddingBottom()) / 2;
167         canvas.save();
168         canvas.translate((canvas.getWidth() - mCenterMarkerRect.right - getPaddingEnd()) / 2,
169                 seekBarCenter - (mCenterMarkerRect.bottom / 2));
170         canvas.drawRect(mCenterMarkerRect, mCenterMarkerPaint);
171         canvas.restore();
172         super.onDraw(canvas);
173     }
174 
createStateDescription(Context context, @StringRes int resIdLeftFirst, @StringRes int resIdRightFirst, int progress, float max)175     private static CharSequence createStateDescription(Context context,
176             @StringRes int resIdLeftFirst, @StringRes int resIdRightFirst,
177             int progress, float max) {
178         final boolean isLayoutRtl = context.getResources().getConfiguration().getLayoutDirection()
179                 == LAYOUT_DIRECTION_RTL;
180         final int rightPercent = (int) (100 * (progress / max));
181         final int leftPercent = 100 - rightPercent;
182         final String rightPercentString = Utils.formatPercentage(rightPercent);
183         final String leftPercentString = Utils.formatPercentage(leftPercent);
184         if (rightPercent > leftPercent || (rightPercent == leftPercent && isLayoutRtl)) {
185             return context.getString(resIdRightFirst, rightPercentString, leftPercentString);
186         } else {
187             return context.getString(resIdLeftFirst, leftPercentString, rightPercentString);
188         }
189     }
190 }
191 
192