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