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