1 /* 2 * Copyright (C) 2016 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.widget; 18 19 import static android.view.HapticFeedbackConstants.CLOCK_TICK; 20 21 import android.content.Context; 22 import android.graphics.Rect; 23 import android.os.Bundle; 24 import android.util.AttributeSet; 25 import android.view.MotionEvent; 26 import android.view.View; 27 import android.view.accessibility.AccessibilityEvent; 28 import android.widget.RadioButton; 29 import android.widget.RadioGroup; 30 import android.widget.SeekBar; 31 32 import androidx.core.view.ViewCompat; 33 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 34 import androidx.customview.widget.ExploreByTouchHelper; 35 36 import java.util.List; 37 38 /** 39 * LabeledSeekBar represent a seek bar assigned with labeled, discrete values. 40 * It pretends to be a group of radio button for AccessibilityServices, in order to adjust the 41 * behavior of these services to keep the mental model of the visual discrete SeekBar. 42 */ 43 public class LabeledSeekBar extends SeekBar { 44 45 private final ExploreByTouchHelper mAccessHelper; 46 47 /** Seek bar change listener set via public method. */ 48 private OnSeekBarChangeListener mOnSeekBarChangeListener; 49 50 /** Labels for discrete progress values. */ 51 private String[] mLabels; 52 53 private int mLastProgress = -1; 54 LabeledSeekBar(Context context, AttributeSet attrs)55 public LabeledSeekBar(Context context, AttributeSet attrs) { 56 this(context, attrs, com.android.internal.R.attr.seekBarStyle); 57 } 58 LabeledSeekBar(Context context, AttributeSet attrs, int defStyleAttr)59 public LabeledSeekBar(Context context, AttributeSet attrs, int defStyleAttr) { 60 this(context, attrs, defStyleAttr, 0); 61 } 62 LabeledSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)63 public LabeledSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 64 super(context, attrs, defStyleAttr, defStyleRes); 65 66 mAccessHelper = new LabeledSeekBarExploreByTouchHelper(this); 67 ViewCompat.setAccessibilityDelegate(this, mAccessHelper); 68 69 super.setOnSeekBarChangeListener(mProxySeekBarListener); 70 } 71 72 @Override setProgress(int progress)73 public synchronized void setProgress(int progress) { 74 // This method gets called from the constructor, so mAccessHelper may 75 // not have been assigned yet. 76 if (mAccessHelper != null) { 77 mAccessHelper.invalidateRoot(); 78 } 79 80 super.setProgress(progress); 81 } 82 setLabels(String[] labels)83 public void setLabels(String[] labels) { 84 mLabels = labels; 85 } 86 87 @Override setOnSeekBarChangeListener(OnSeekBarChangeListener l)88 public void setOnSeekBarChangeListener(OnSeekBarChangeListener l) { 89 // The callback set in the constructor will proxy calls to this 90 // listener. 91 mOnSeekBarChangeListener = l; 92 } 93 94 @Override dispatchHoverEvent(MotionEvent event)95 protected boolean dispatchHoverEvent(MotionEvent event) { 96 return mAccessHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event); 97 } 98 sendClickEventForAccessibility(int progress)99 private void sendClickEventForAccessibility(int progress) { 100 mAccessHelper.invalidateRoot(); 101 mAccessHelper.sendEventForVirtualView(progress, AccessibilityEvent.TYPE_VIEW_CLICKED); 102 } 103 104 private final OnSeekBarChangeListener mProxySeekBarListener = new OnSeekBarChangeListener() { 105 @Override 106 public void onStopTrackingTouch(SeekBar seekBar) { 107 if (mOnSeekBarChangeListener != null) { 108 mOnSeekBarChangeListener.onStopTrackingTouch(seekBar); 109 } 110 } 111 112 @Override 113 public void onStartTrackingTouch(SeekBar seekBar) { 114 if (mOnSeekBarChangeListener != null) { 115 mOnSeekBarChangeListener.onStartTrackingTouch(seekBar); 116 } 117 } 118 119 @Override 120 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 121 if (mOnSeekBarChangeListener != null) { 122 mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser); 123 sendClickEventForAccessibility(progress); 124 } 125 if (progress != mLastProgress) { 126 seekBar.performHapticFeedback(CLOCK_TICK); 127 mLastProgress = progress; 128 } 129 } 130 }; 131 132 private class LabeledSeekBarExploreByTouchHelper extends ExploreByTouchHelper { 133 134 private boolean mIsLayoutRtl; 135 LabeledSeekBarExploreByTouchHelper(LabeledSeekBar forView)136 public LabeledSeekBarExploreByTouchHelper(LabeledSeekBar forView) { 137 super(forView); 138 mIsLayoutRtl = forView.getResources().getConfiguration() 139 .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 140 } 141 142 @Override getVirtualViewAt(float x, float y)143 protected int getVirtualViewAt(float x, float y) { 144 return getVirtualViewIdIndexFromX(x); 145 } 146 147 @Override getVisibleVirtualViews(List<Integer> list)148 protected void getVisibleVirtualViews(List<Integer> list) { 149 for (int i = 0, c = LabeledSeekBar.this.getMax(); i <= c; ++i) { 150 list.add(i); 151 } 152 } 153 154 @Override onPerformActionForVirtualView(int virtualViewId, int action, Bundle arguments)155 protected boolean onPerformActionForVirtualView(int virtualViewId, int action, 156 Bundle arguments) { 157 if (virtualViewId == ExploreByTouchHelper.HOST_ID) { 158 // Do nothing 159 return false; 160 } 161 162 switch (action) { 163 case AccessibilityNodeInfoCompat.ACTION_CLICK: 164 LabeledSeekBar.this.setProgress(virtualViewId); 165 sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED); 166 return true; 167 default: 168 return false; 169 } 170 } 171 172 @Override onPopulateNodeForVirtualView( int virtualViewId, AccessibilityNodeInfoCompat node)173 protected void onPopulateNodeForVirtualView( 174 int virtualViewId, AccessibilityNodeInfoCompat node) { 175 node.setClassName(RadioButton.class.getName()); 176 node.setBoundsInParent(getBoundsInParentFromVirtualViewId(virtualViewId)); 177 node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); 178 node.setContentDescription(mLabels[virtualViewId]); 179 node.setClickable(true); 180 node.setCheckable(true); 181 node.setChecked(virtualViewId == LabeledSeekBar.this.getProgress()); 182 } 183 184 @Override onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event)185 protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { 186 event.setClassName(RadioButton.class.getName()); 187 event.setContentDescription(mLabels[virtualViewId]); 188 event.setChecked(virtualViewId == LabeledSeekBar.this.getProgress()); 189 } 190 191 @Override onPopulateNodeForHost(AccessibilityNodeInfoCompat node)192 protected void onPopulateNodeForHost(AccessibilityNodeInfoCompat node) { 193 node.setClassName(RadioGroup.class.getName()); 194 } 195 196 @Override onPopulateEventForHost(AccessibilityEvent event)197 protected void onPopulateEventForHost(AccessibilityEvent event) { 198 event.setClassName(RadioGroup.class.getName()); 199 } 200 getHalfVirtualViewWidth()201 private int getHalfVirtualViewWidth() { 202 final int width = LabeledSeekBar.this.getWidth(); 203 final int barWidth = width - LabeledSeekBar.this.getPaddingStart() 204 - LabeledSeekBar.this.getPaddingEnd(); 205 return Math.max(0, barWidth / (LabeledSeekBar.this.getMax() * 2)); 206 } 207 getVirtualViewIdIndexFromX(float x)208 private int getVirtualViewIdIndexFromX(float x) { 209 int posBase = Math.max(0, 210 ((int) x - LabeledSeekBar.this.getPaddingStart()) / getHalfVirtualViewWidth()); 211 posBase = (posBase + 1) / 2; 212 posBase = Math.min(posBase, LabeledSeekBar.this.getMax()); 213 return mIsLayoutRtl ? LabeledSeekBar.this.getMax() - posBase : posBase; 214 } 215 getBoundsInParentFromVirtualViewId(int virtualViewId)216 private Rect getBoundsInParentFromVirtualViewId(int virtualViewId) { 217 final int updatedVirtualViewId = mIsLayoutRtl 218 ? LabeledSeekBar.this.getMax() - virtualViewId : virtualViewId; 219 int left = (updatedVirtualViewId * 2 - 1) * getHalfVirtualViewWidth() 220 + LabeledSeekBar.this.getPaddingStart(); 221 int right = (updatedVirtualViewId * 2 + 1) * getHalfVirtualViewWidth() 222 + LabeledSeekBar.this.getPaddingStart(); 223 224 // Edge case 225 left = updatedVirtualViewId == 0 ? 0 : left; 226 right = updatedVirtualViewId == LabeledSeekBar.this.getMax() 227 ? LabeledSeekBar.this.getWidth() : right; 228 229 final Rect r = new Rect(); 230 r.set(left, 0, right, LabeledSeekBar.this.getHeight()); 231 return r; 232 } 233 } 234 } 235