Home | History | Annotate | Download | only in com.example.android.basicaccessibility
      1 /*
      2  * Copyright (C) 2013 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.example.android.basicaccessibility;
     18 
     19 import android.annotation.TargetApi;
     20 import android.content.Context;
     21 import android.graphics.Canvas;
     22 import android.graphics.Color;
     23 import android.graphics.Paint;
     24 import android.os.Build;
     25 import android.util.AttributeSet;
     26 import android.view.View;
     27 import android.view.accessibility.AccessibilityEvent;
     28 
     29 /**
     30  * Custom view to demonstrate accessibility.
     31  *
     32  * <p>This view does not use any framework widgets, so does not get any accessibility features
     33  * automatically. Instead, we use {@link android.view.accessibility.AccessibilityEvent} to provide accessibility hints to
     34  * the OS.
     35  *
     36  * <p>For example, if TalkBack is enabled, users will be able to receive spoken feedback as they
     37  * interact with this view.
     38  *
     39  * <p>More generally, this view renders a multi-position "dial" that can be used to select a value
     40  * between 1 and 4. Each time the dial is clicked, the next position will be selected (modulo
     41  * the maximum number of positions).
     42  */
     43 public class DialView extends View {
     44     private static int SELECTION_COUNT = 4;
     45 
     46     private static float FONT_SIZE = 40f;
     47     private float mWidth;
     48     private float mHeight;
     49     private float mWidthPadded;
     50     private float mHeightPadded;
     51     private Paint mTextPaint;
     52     private Paint mDialPaint;
     53     private float mRadius;
     54     private int mActiveSelection;
     55 
     56     /**
     57      * Constructor that is called when inflating a view from XML. This is called
     58      * when a view is being constructed from an XML file, supplying attributes
     59      * that were specified in the XML file.
     60      *
     61      * <p>In our case, this constructor just calls init().
     62      *
     63      * @param context The Context the view is running in, through which it can
     64      *                access the current theme, resources, etc.
     65      * @param attrs   The attributes of the XML tag that is inflating the view.
     66      * @see #View(android.content.Context, android.util.AttributeSet, int)
     67      */
     68     public DialView(Context context, AttributeSet attrs) {
     69         super(context, attrs);
     70         init();
     71     }
     72 
     73     /**
     74      * Helper method to initialize instance variables. Called by constructor.
     75      */
     76     private void init() {
     77         // Paint styles used for rendering are created here, rather than at render-time. This
     78         // is a performance optimization, since onDraw() will get called frequently.
     79         mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
     80         mTextPaint.setColor(Color.BLACK);
     81         mTextPaint.setStyle(Paint.Style.FILL_AND_STROKE);
     82         mTextPaint.setTextAlign(Paint.Align.CENTER);
     83         mTextPaint.setTextSize(FONT_SIZE);
     84 
     85         mDialPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
     86         mDialPaint.setColor(Color.GRAY);
     87 
     88         // Initialize current selection. This will store where the dial's "indicator" is pointing.
     89         mActiveSelection = 0;
     90 
     91         // Setup onClick listener for this view. Rotates between each of the different selection
     92         // states on each click.
     93         //
     94         // Notice that we call sendAccessibilityEvent here. Some AccessibilityEvents are generated
     95         // by the system. However, custom views will typically need to send events manually as the
     96         // user interacts with the view. The type of event sent will vary, depending on the nature
     97         // of the view and how the user interacts with it.
     98         //
     99         // In this case, we are sending TYPE_VIEW_SELECTED rather than TYPE_VIEW_CLICKED, because
    100         // clicking on this view selects a new value.
    101         //
    102         // We will give our AccessibilityEvent further information about the state of the view in
    103         // onPopulateAccessibilityEvent(), which will be called automatically by the system
    104         // for each AccessibilityEvent.
    105         setOnClickListener(new OnClickListener() {
    106             @Override
    107             public void onClick(View v) {
    108                 // Rotate selection to the next valid choice.
    109                 mActiveSelection = (mActiveSelection + 1) % SELECTION_COUNT;
    110                 // Send an AccessibilityEvent, since the user has interacted with the view.
    111                 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
    112                 // Redraw the entire view. (Inefficient, but this is sufficient for demonstration
    113                 // purposes.)
    114                 invalidate();
    115             }
    116         });
    117     }
    118 
    119     /**
    120      * This is where a View should populate outgoing accessibility events with its text content.
    121      * While this method is free to modify event attributes other than text content, doing so
    122      * should normally be performed in
    123      * {@link #onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent)}.
    124      * <p/>
    125      * <p>Note that the behavior of this method will typically vary, depending on the type of
    126      * accessibility event is passed into it. The allowed values also very, and are documented
    127      * in {@link android.view.accessibility.AccessibilityEvent}.
    128      * <p/>
    129      * <p>Typically, this is where you'll describe the state of your custom view. You may also
    130      * want to provide custom directions when the user has focused your view.
    131      *
    132      * @param event The accessibility event which to populate.
    133      */
    134     // BEGIN_INCLUDE (on_populate_accessibility_event)
    135     @Override
    136     @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    137     public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
    138         super.onPopulateAccessibilityEvent(event);
    139 
    140         // Detect what type of accessibility event is being passed in.
    141         int eventType = event.getEventType();
    142 
    143         // Common case: The user has interacted with our view in some way. State may or may not
    144         // have been changed. Read out the current status of the view.
    145         //
    146         // We also set some other metadata which is not used by TalkBack, but could be used by
    147         // other TTS engines.
    148         if (eventType == AccessibilityEvent.TYPE_VIEW_SELECTED ||
    149                 eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
    150             event.getText().add("Mode selected: " + Integer.toString(mActiveSelection + 1) + ".");
    151             event.setItemCount(SELECTION_COUNT);
    152             event.setCurrentItemIndex(mActiveSelection);
    153         }
    154 
    155         // When a user first focuses on our view, we'll also read out some simple instructions to
    156         // make it clear that this is an interactive element.
    157         if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
    158             event.getText().add("Tap to change.");
    159         }
    160     }
    161     // END_INCLUDE (on_populate_accessibility_event)
    162 
    163     /**
    164      * This is called during layout when the size of this view has changed. If
    165      * you were just added to the view hierarchy, you're called with the old
    166      * values of 0.
    167      *
    168      * <p>This is where we determine the drawing bounds for our custom view.
    169      *
    170      * @param w    Current width of this view.
    171      * @param h    Current height of this view.
    172      * @param oldw Old width of this view.
    173      * @param oldh Old height of this view.
    174      */
    175     @Override
    176     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    177         // Account for padding
    178         float xPadding = (float) (getPaddingLeft() + getPaddingRight());
    179         float yPadding = (float) (getPaddingTop() + getPaddingBottom());
    180 
    181         // Compute available width/height
    182         mWidth = w;
    183         mHeight = h;
    184         mWidthPadded = w - xPadding;
    185         mHeightPadded = h - yPadding;
    186         mRadius = (float) (Math.min(mWidth, mHeight) / 2 * 0.8);
    187     }
    188 
    189     /**
    190      * Render view content.
    191      *
    192      * <p>We render an outer grey circle to serve as our "dial", and then render a smaller black
    193      * circle to server as our indicator. The position for the indicator is determined based
    194      * on mActiveSelection.
    195      *
    196      * @param canvas the canvas on which the background will be drawn
    197      */
    198     @Override
    199     protected void onDraw(Canvas canvas) {
    200         super.onDraw(canvas);
    201         // Draw dial
    202         canvas.drawCircle(mWidth / 2, mHeight / 2, (float) mRadius, mDialPaint);
    203 
    204         // Draw text labels
    205         final float labelRadius = mRadius + 10;
    206         for (int i = 0; i < SELECTION_COUNT; i++) {
    207             float[] xyData = computeXYForPosition(i, labelRadius);
    208             float x = xyData[0];
    209             float y = xyData[1];
    210             canvas.drawText(Integer.toString(i + 1), x, y, mTextPaint);
    211         }
    212 
    213         // Draw indicator mark
    214         final float markerRadius = mRadius - 35;
    215         float[] xyData = computeXYForPosition(mActiveSelection, markerRadius);
    216         float x = xyData[0];
    217         float y = xyData[1];
    218         canvas.drawCircle(x, y, 20, mTextPaint);
    219     }
    220 
    221     /**
    222      * Compute the X/Y-coordinates for a label or indicator, given the position number and radius
    223      * where the label should be drawn.
    224      *
    225      * @param pos    Zero based position index
    226      * @param radius Radius where label/indicator is to be drawn.
    227      * @return 2-element array. Element 0 is X-coordinate, element 1 is Y-coordinate.
    228      */
    229     private float[] computeXYForPosition(final int pos, final float radius) {
    230         float[] result = new float[2];
    231         Double startAngle = Math.PI * (9 / 8d);   // Angles are in radiansq
    232         Double angle = startAngle + (pos * (Math.PI / 4));
    233         result[0] = (float) (radius * Math.cos(angle)) + (mWidth / 2);
    234         result[1] = (float) (radius * Math.sin(angle)) + (mHeight / 2);
    235         return result;
    236     }
    237 }
    238