Add sample code for ExploreByTouchHelper class.

Change-Id: Id8f0ba809746114434d553885ddfed9260bdaa45
diff --git a/samples/Support4Demos/AndroidManifest.xml b/samples/Support4Demos/AndroidManifest.xml
index 1b015f1..2d7f732 100644
--- a/samples/Support4Demos/AndroidManifest.xml
+++ b/samples/Support4Demos/AndroidManifest.xml
@@ -312,6 +312,14 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".widget.ExploreByTouchHelperActivity"
+                  android:label="@string/explore_by_touch_helper_support">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="com.example.android.supportv4.SUPPORT4_SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+
         <provider android:authorities="com.example.supportv4.content.sharingsupportprovider"
                   android:name=".content.SharingSupportProvider" />
 
diff --git a/samples/Support4Demos/res/layout/explore_by_touch_helper.xml b/samples/Support4Demos/res/layout/explore_by_touch_helper.xml
new file mode 100644
index 0000000..0b367a4
--- /dev/null
+++ b/samples/Support4Demos/res/layout/explore_by_touch_helper.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+
+    <view
+        class="com.example.android.supportv4.widget.ExploreByTouchHelperActivity$CustomView"
+        android:id="@+id/custom_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/Support4Demos/res/values/strings.xml b/samples/Support4Demos/res/values/strings.xml
index 5860ccc..afddd7f 100644
--- a/samples/Support4Demos/res/values/strings.xml
+++ b/samples/Support4Demos/res/values/strings.xml
@@ -166,4 +166,10 @@
 
     <string name="sliding_pane_layout_summary">This activity illustrates the use of sliding panes. The content pane may be slid to one side on narrow devices to reveal the left pane used to select content. Sliding panes can be used to fit a UI intended for wider screens in a smaller space. Tapping the Action Bar\'s Up button at the left side of the bar will navigate up in the hierarchy, represented by the left pane. If you rotate the device to landscape mode, on most devices you will see that both panes fit together side by side with no sliding necessary.</string>
 
+    <!-- ExploreByTouchHelper -->
+
+    <string name="explore_by_touch_helper_support">Widget/Explore by Touch helper</string>
+    <string name="sample_item_a">Sample item A</string>
+    <string name="sample_item_b">Sample item B</string>
+
 </resources>
diff --git a/samples/Support4Demos/src/com/example/android/supportv4/widget/ExploreByTouchHelperActivity.java b/samples/Support4Demos/src/com/example/android/supportv4/widget/ExploreByTouchHelperActivity.java
new file mode 100644
index 0000000..946de01
--- /dev/null
+++ b/samples/Support4Demos/src/com/example/android/supportv4/widget/ExploreByTouchHelperActivity.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2013 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.example.android.supportv4.widget;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Paint.Style;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
+import android.support.v4.widget.ExploreByTouchHelper;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import com.example.android.supportv4.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This example shows how to use the {@link ExploreByTouchHelper} class in the
+ * Android support library to add accessibility support to a custom view that
+ * represents multiple logical items.
+ * <p>
+ * The {@link ExploreByTouchHelper} class wraps
+ * {@link AccessibilityNodeProviderCompat} and simplifies exposing information
+ * about a custom view's logical structure to accessibility services.
+ * <p>
+ * The custom view in this example is responsible for:
+ * <ul>
+ * <li>Creating a helper class that extends {@link ExploreByTouchHelper}
+ * <li>Setting the helper as the accessibility delegate using
+ * {@link ViewCompat#setAccessibilityDelegate}
+ * <li>Dispatching hover events to the helper in {@link View#dispatchHoverEvent}
+ * </ul>
+ * <p>
+ * The helper class implementation in this example is responsible for:
+ * <ul>
+ * <li>Mapping hover event coordinates to logical items
+ * <li>Exposing information about logical items to accessibility services
+ * <li>Handling accessibility actions
+ * <ul>
+ */
+public class ExploreByTouchHelperActivity extends Activity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.explore_by_touch_helper);
+
+        final CustomView customView = (CustomView) findViewById(R.id.custom_view);
+
+        // Adds an item at the top-left quarter of the custom view.
+        customView.addItem(getString(R.string.sample_item_a), 0, 0, 0.5f, 0.5f);
+
+        // Adds an item at the bottom-right quarter of the custom view.
+        customView.addItem(getString(R.string.sample_item_b), 0.5f, 0.5f, 1, 1);
+    }
+
+    /**
+     * Simple custom view that draws rectangular items to the screen. Each item
+     * has a checked state that may be toggled by tapping on the item.
+     */
+    public static class CustomView extends View {
+        private static final int NO_ITEM = -1;
+
+        private final Paint mPaint = new Paint();
+        private final Rect mTempBounds = new Rect();
+        private final List<CustomItem> mItems = new ArrayList<CustomItem>();
+        private CustomViewTouchHelper mTouchHelper;
+
+        public CustomView(Context context, AttributeSet attrs) {
+            super(context, attrs);
+
+            // Set up accessibility helper class.
+            mTouchHelper = new CustomViewTouchHelper(this);
+            ViewCompat.setAccessibilityDelegate(this, mTouchHelper);
+        }
+
+        @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+        @Override
+        public boolean dispatchHoverEvent(MotionEvent event) {
+            // Always attempt to dispatch hover events to accessibility first.
+            if (mTouchHelper.dispatchHoverEvent(event)) {
+                return true;
+            }
+
+            return super.dispatchHoverEvent(event);
+        }
+
+        @Override
+        public boolean onTouchEvent(MotionEvent event) {
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_DOWN:
+                    return true;
+                case MotionEvent.ACTION_UP:
+                    final int itemIndex = getItemIndexUnder(event.getX(), event.getY());
+                    if (itemIndex >= 0) {
+                        onItemClicked(itemIndex);
+                    }
+                    return true;
+            }
+
+            return super.onTouchEvent(event);
+        }
+
+        /**
+         * Adds an item to the custom view. The item is positioned relative to
+         * the custom view bounds and its descriptions is drawn at its center.
+         *
+         * @param description The item's description.
+         * @param top Top coordinate as a fraction of the parent height, range
+         *            is [0,1].
+         * @param left Left coordinate as a fraction of the parent width, range
+         *            is [0,1].
+         * @param bottom Bottom coordinate as a fraction of the parent height,
+         *            range is [0,1].
+         * @param right Right coordinate as a fraction of the parent width,
+         *            range is [0,1].
+         */
+        public void addItem(String description, float top, float left, float bottom, float right) {
+            final CustomItem item = new CustomItem();
+            item.bounds = new RectF(top, left, bottom, right);
+            item.description = description;
+            item.checked = false;
+            mItems.add(item);
+        }
+
+        @Override
+        protected void onDraw(Canvas canvas) {
+            super.onDraw(canvas);
+
+            final Paint paint = mPaint;
+            final Rect bounds = mTempBounds;
+            final int height = getHeight();
+            final int width = getWidth();
+
+            for (CustomItem item : mItems) {
+                paint.setColor(item.checked ? Color.RED : Color.BLUE);
+                paint.setStyle(Style.FILL);
+                scaleRectF(item.bounds, bounds, width, height);
+                canvas.drawRect(bounds, paint);
+                paint.setColor(Color.WHITE);
+                paint.setTextAlign(Align.CENTER);
+                canvas.drawText(item.description, bounds.centerX(), bounds.centerY(), paint);
+            }
+        }
+
+        protected boolean onItemClicked(int index) {
+            final CustomItem item = getItem(index);
+            if (item == null) {
+                return false;
+            }
+
+            item.checked = !item.checked;
+            invalidate();
+
+            // Since the item's checked state is exposed to accessibility
+            // services through its AccessibilityNodeInfo, we need to invalidate
+            // the item's virtual view. At some point in the future, the
+            // framework will obtain an updated version of the virtual view.
+            mTouchHelper.invalidateVirtualView(index);
+
+            // We also need to let the framework know what type of event
+            // happened. Accessibility services may use this event to provide
+            // appropriate feedback to the user.
+            mTouchHelper.sendEventForVirtualView(index, AccessibilityEvent.TYPE_VIEW_CLICKED);
+
+            return true;
+        }
+
+        protected int getItemIndexUnder(float x, float y) {
+            final float scaledX = (x / getWidth());
+            final float scaledY = (y / getHeight());
+            final int n = mItems.size();
+
+            for (int i = 0; i < n; i++) {
+                final CustomItem item = mItems.get(i);
+                if (item.bounds.contains(scaledX, scaledY)) {
+                    return i;
+                }
+            }
+
+            return NO_ITEM;
+        }
+
+        protected CustomItem getItem(int index) {
+            if ((index < 0) || (index >= mItems.size())) {
+                return null;
+            }
+
+            return mItems.get(index);
+        }
+
+        protected static void scaleRectF(RectF in, Rect out, int width, int height) {
+            out.top = (int) (in.top * height);
+            out.bottom = (int) (in.bottom * height);
+            out.left = (int) (in.left * width);
+            out.right = (int) (in.right * width);
+        }
+
+        private class CustomViewTouchHelper extends ExploreByTouchHelper {
+            private final Rect mTempRect = new Rect();
+
+            public CustomViewTouchHelper(View forView) {
+                super(forView);
+            }
+
+            @Override
+            protected int getVirtualViewAt(float x, float y) {
+                // We also perform hit detection in onTouchEvent(), and we can
+                // reuse that logic here. This will ensure consistency whether
+                // accessibility is on or off.
+                final int index = getItemIndexUnder(x, y);
+                if (index == NO_ITEM) {
+                    return ExploreByTouchHelper.INVALID_ID;
+                }
+
+                return index;
+            }
+
+            @Override
+            protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
+                // Since every item should be visible, and since we're mapping
+                // directly from item index to virtual view id, we can just add
+                // every available index in the item list.
+                final int n = mItems.size();
+                for (int i = 0; i < n; i++) {
+                    virtualViewIds.add(i);
+                }
+            }
+
+            @Override
+            protected void onPopulateEventForVirtualView(
+                    int virtualViewId, AccessibilityEvent event) {
+                final CustomItem item = getItem(virtualViewId);
+                if (item == null) {
+                    throw new IllegalArgumentException("Invalid virtual view id");
+                }
+
+                // The event must be populated with text, either using
+                // getText().add() or setContentDescription(). Since the item's
+                // description is displayed visually, we'll add it to the event
+                // text. If it was only used for accessibility, we would use
+                // setContentDescription().
+                event.getText().add(item.description);
+            }
+
+            @Override
+            protected void onPopulateNodeForVirtualView(
+                    int virtualViewId, AccessibilityNodeInfoCompat node) {
+                final CustomItem item = getItem(virtualViewId);
+                if (item == null) {
+                    throw new IllegalArgumentException("Invalid virtual view id");
+                }
+
+                // Node and event text and content descriptions are usually
+                // identical, so we'll use the exact same string as before.
+                node.setText(item.description);
+
+                // Reported bounds should be consistent with those used to draw
+                // the item in onDraw(). They should also be consistent with the
+                // hit detection performed in getVirtualViewAt() and
+                // onTouchEvent().
+                final Rect bounds = mTempRect;
+                final int height = getHeight();
+                final int width = getWidth();
+                scaleRectF(item.bounds, bounds, width, height);
+                node.setBoundsInParent(bounds);
+
+                // Since the user can tap an item, add the CLICK action. We'll
+                // need to handle this later in onPerformActionForVirtualView.
+                node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
+
+                // This item has a checked state.
+                node.setCheckable(true);
+                node.setChecked(item.checked);
+            }
+
+            @Override
+            protected boolean onPerformActionForVirtualView(
+                    int virtualViewId, int action, Bundle arguments) {
+                switch (action) {
+                    case AccessibilityNodeInfoCompat.ACTION_CLICK:
+                        // Click handling should be consistent with
+                        // onTouchEvent(). This ensures that the view works the
+                        // same whether accessibility is turned on or off.
+                        return onItemClicked(virtualViewId);
+                }
+
+                return false;
+            }
+
+        }
+
+        public static class CustomItem {
+            private String description;
+            private RectF bounds;
+            private boolean checked;
+        }
+    }
+}