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