| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.settings.widget; |
| |
| import static android.view.HapticFeedbackConstants.CLOCK_TICK; |
| |
| import android.content.Context; |
| import android.graphics.Rect; |
| import android.os.Bundle; |
| import android.util.AttributeSet; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.widget.RadioButton; |
| import android.widget.RadioGroup; |
| import android.widget.SeekBar; |
| |
| import androidx.core.view.ViewCompat; |
| import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; |
| import androidx.customview.widget.ExploreByTouchHelper; |
| |
| import java.util.List; |
| |
| /** |
| * LabeledSeekBar represent a seek bar assigned with labeled, discrete values. |
| * It pretends to be a group of radio button for AccessibilityServices, in order to adjust the |
| * behavior of these services to keep the mental model of the visual discrete SeekBar. |
| */ |
| public class LabeledSeekBar extends SeekBar { |
| |
| private final ExploreByTouchHelper mAccessHelper; |
| |
| /** Seek bar change listener set via public method. */ |
| private OnSeekBarChangeListener mOnSeekBarChangeListener; |
| |
| /** Labels for discrete progress values. */ |
| private String[] mLabels; |
| |
| private int mLastProgress = -1; |
| |
| public LabeledSeekBar(Context context, AttributeSet attrs) { |
| this(context, attrs, com.android.internal.R.attr.seekBarStyle); |
| } |
| |
| public LabeledSeekBar(Context context, AttributeSet attrs, int defStyleAttr) { |
| this(context, attrs, defStyleAttr, 0); |
| } |
| |
| public LabeledSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { |
| super(context, attrs, defStyleAttr, defStyleRes); |
| |
| mAccessHelper = new LabeledSeekBarExploreByTouchHelper(this); |
| ViewCompat.setAccessibilityDelegate(this, mAccessHelper); |
| |
| super.setOnSeekBarChangeListener(mProxySeekBarListener); |
| } |
| |
| @Override |
| public synchronized void setProgress(int progress) { |
| // This method gets called from the constructor, so mAccessHelper may |
| // not have been assigned yet. |
| if (mAccessHelper != null) { |
| mAccessHelper.invalidateRoot(); |
| } |
| |
| super.setProgress(progress); |
| } |
| |
| public void setLabels(String[] labels) { |
| mLabels = labels; |
| } |
| |
| @Override |
| public void setOnSeekBarChangeListener(OnSeekBarChangeListener l) { |
| // The callback set in the constructor will proxy calls to this |
| // listener. |
| mOnSeekBarChangeListener = l; |
| } |
| |
| @Override |
| protected boolean dispatchHoverEvent(MotionEvent event) { |
| return mAccessHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event); |
| } |
| |
| private void sendClickEventForAccessibility(int progress) { |
| mAccessHelper.invalidateRoot(); |
| mAccessHelper.sendEventForVirtualView(progress, AccessibilityEvent.TYPE_VIEW_CLICKED); |
| } |
| |
| private final OnSeekBarChangeListener mProxySeekBarListener = new OnSeekBarChangeListener() { |
| @Override |
| public void onStopTrackingTouch(SeekBar seekBar) { |
| if (mOnSeekBarChangeListener != null) { |
| mOnSeekBarChangeListener.onStopTrackingTouch(seekBar); |
| } |
| } |
| |
| @Override |
| public void onStartTrackingTouch(SeekBar seekBar) { |
| if (mOnSeekBarChangeListener != null) { |
| mOnSeekBarChangeListener.onStartTrackingTouch(seekBar); |
| } |
| } |
| |
| @Override |
| public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { |
| if (mOnSeekBarChangeListener != null) { |
| mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser); |
| sendClickEventForAccessibility(progress); |
| } |
| if (progress != mLastProgress) { |
| seekBar.performHapticFeedback(CLOCK_TICK); |
| mLastProgress = progress; |
| } |
| } |
| }; |
| |
| private class LabeledSeekBarExploreByTouchHelper extends ExploreByTouchHelper { |
| |
| private boolean mIsLayoutRtl; |
| |
| public LabeledSeekBarExploreByTouchHelper(LabeledSeekBar forView) { |
| super(forView); |
| mIsLayoutRtl = forView.getResources().getConfiguration() |
| .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; |
| } |
| |
| @Override |
| protected int getVirtualViewAt(float x, float y) { |
| return getVirtualViewIdIndexFromX(x); |
| } |
| |
| @Override |
| protected void getVisibleVirtualViews(List<Integer> list) { |
| for (int i = 0, c = LabeledSeekBar.this.getMax(); i <= c; ++i) { |
| list.add(i); |
| } |
| } |
| |
| @Override |
| protected boolean onPerformActionForVirtualView(int virtualViewId, int action, |
| Bundle arguments) { |
| if (virtualViewId == ExploreByTouchHelper.HOST_ID) { |
| // Do nothing |
| return false; |
| } |
| |
| switch (action) { |
| case AccessibilityNodeInfoCompat.ACTION_CLICK: |
| LabeledSeekBar.this.setProgress(virtualViewId); |
| sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED); |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| @Override |
| protected void onPopulateNodeForVirtualView( |
| int virtualViewId, AccessibilityNodeInfoCompat node) { |
| node.setClassName(RadioButton.class.getName()); |
| node.setBoundsInParent(getBoundsInParentFromVirtualViewId(virtualViewId)); |
| node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); |
| node.setContentDescription(mLabels[virtualViewId]); |
| node.setClickable(true); |
| node.setCheckable(true); |
| node.setChecked(virtualViewId == LabeledSeekBar.this.getProgress()); |
| } |
| |
| @Override |
| protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { |
| event.setClassName(RadioButton.class.getName()); |
| event.setContentDescription(mLabels[virtualViewId]); |
| event.setChecked(virtualViewId == LabeledSeekBar.this.getProgress()); |
| } |
| |
| @Override |
| protected void onPopulateNodeForHost(AccessibilityNodeInfoCompat node) { |
| node.setClassName(RadioGroup.class.getName()); |
| } |
| |
| @Override |
| protected void onPopulateEventForHost(AccessibilityEvent event) { |
| event.setClassName(RadioGroup.class.getName()); |
| } |
| |
| private int getHalfVirtualViewWidth() { |
| final int width = LabeledSeekBar.this.getWidth(); |
| final int barWidth = width - LabeledSeekBar.this.getPaddingStart() |
| - LabeledSeekBar.this.getPaddingEnd(); |
| return Math.max(0, barWidth / (LabeledSeekBar.this.getMax() * 2)); |
| } |
| |
| private int getVirtualViewIdIndexFromX(float x) { |
| int posBase = Math.max(0, |
| ((int) x - LabeledSeekBar.this.getPaddingStart()) / getHalfVirtualViewWidth()); |
| posBase = (posBase + 1) / 2; |
| posBase = Math.min(posBase, LabeledSeekBar.this.getMax()); |
| return mIsLayoutRtl ? LabeledSeekBar.this.getMax() - posBase : posBase; |
| } |
| |
| private Rect getBoundsInParentFromVirtualViewId(int virtualViewId) { |
| final int updatedVirtualViewId = mIsLayoutRtl |
| ? LabeledSeekBar.this.getMax() - virtualViewId : virtualViewId; |
| int left = (updatedVirtualViewId * 2 - 1) * getHalfVirtualViewWidth() |
| + LabeledSeekBar.this.getPaddingStart(); |
| int right = (updatedVirtualViewId * 2 + 1) * getHalfVirtualViewWidth() |
| + LabeledSeekBar.this.getPaddingStart(); |
| |
| // Edge case |
| left = updatedVirtualViewId == 0 ? 0 : left; |
| right = updatedVirtualViewId == LabeledSeekBar.this.getMax() |
| ? LabeledSeekBar.this.getWidth() : right; |
| |
| final Rect r = new Rect(); |
| r.set(left, 0, right, LabeledSeekBar.this.getHeight()); |
| return r; |
| } |
| } |
| } |