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