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