Home | History | Annotate | Download | only in widget
      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 android.content.Context;
     20 import android.content.res.Configuration;
     21 import android.graphics.Rect;
     22 import android.os.Bundle;
     23 import android.support.v4.view.ViewCompat;
     24 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
     25 import android.support.v4.widget.ExploreByTouchHelper;
     26 import android.util.AttributeSet;
     27 import android.view.MotionEvent;
     28 import android.view.View;
     29 import android.view.accessibility.AccessibilityEvent;
     30 import android.widget.RadioButton;
     31 import android.widget.RadioGroup;
     32 import android.widget.SeekBar;
     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 
     51     public LabeledSeekBar(Context context, AttributeSet attrs) {
     52         this(context, attrs, com.android.internal.R.attr.seekBarStyle);
     53     }
     54 
     55     public LabeledSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
     56         this(context, attrs, defStyleAttr, 0);
     57     }
     58 
     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
     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 
     79     public void setLabels(String[] labels) {
     80         mLabels = labels;
     81     }
     82 
     83     @Override
     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
     91     protected boolean dispatchHoverEvent(MotionEvent event) {
     92         return mAccessHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
     93     }
     94 
     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 
    128         public LabeledSeekBarExploreByTouchHelper(LabeledSeekBar forView) {
    129             super(forView);
    130             mIsLayoutRtl = forView.getResources().getConfiguration()
    131                     .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
    132         }
    133 
    134         @Override
    135         protected int getVirtualViewAt(float x, float y) {
    136             return getVirtualViewIdIndexFromX(x);
    137         }
    138 
    139         @Override
    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
    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
    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
    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
    184         protected void onPopulateNodeForHost(AccessibilityNodeInfoCompat node) {
    185             node.setClassName(RadioGroup.class.getName());
    186         }
    187 
    188         @Override
    189         protected void onPopulateEventForHost(AccessibilityEvent event) {
    190             event.setClassName(RadioGroup.class.getName());
    191         }
    192 
    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 
    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 
    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