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