A-Z scroller in Contact List

Change-Id: Iab421c26a54371e2cb543bcbe53c712b47f52d06
diff --git a/res/drawable-hdpi/aizy_bottom.png b/res/drawable-hdpi/aizy_bottom.png
new file mode 100644
index 0000000..1f3d332
--- /dev/null
+++ b/res/drawable-hdpi/aizy_bottom.png
Binary files differ
diff --git a/res/layout-finger/contacts_list_content.xml b/res/layout-finger/contacts_list_content.xml
index 9ea1c68..f5bb785 100644
--- a/res/layout-finger/contacts_list_content.xml
+++ b/res/layout-finger/contacts_list_content.xml
@@ -19,23 +19,37 @@
         android:id="@+id/pinned_header_list_layout"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:orientation="vertical"
+        android:orientation="horizontal"
         >
 
-    <view
-        class="com.android.contacts.ContactEntryListView" 
-        android:id="@android:id/list"
-        android:layout_width="match_parent"
-        android:layout_height="0dip"
-        android:fastScrollEnabled="true"
-        android:layout_weight="1"
+    <com.android.contacts.list.ContactListAizyView
+        android:id="@+id/contacts_list_aizy"
+        android:layout_width="50dip"
+        android:layout_height="match_parent"
     />
 
-    <include layout="@layout/contacts_list_empty"/>
+    <LinearLayout
+            android:layout_width="0px"
+            android:layout_height="match_parent"
+            android:orientation="vertical"
+            android:layout_weight="1"
+            >
 
-    <ViewStub android:id="@+id/footer_stub"
-        android:layout="@layout/footer_panel"
-        android:layout_width="fill_parent"
-        android:layout_height="wrap_content"
-    />
+        <view
+            class="com.android.contacts.ContactEntryListView"
+            android:id="@android:id/list"
+            android:layout_width="match_parent"
+            android:layout_height="0dip"
+            android:fastScrollEnabled="true"
+            android:layout_weight="1"
+        />
+
+        <include layout="@layout/contacts_list_empty"/>
+
+        <ViewStub android:id="@+id/footer_stub"
+            android:layout="@layout/footer_panel"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+        />
+    </LinearLayout>
 </LinearLayout>
diff --git a/res/layout/aizy_popup_window.xml b/res/layout/aizy_popup_window.xml
new file mode 100644
index 0000000..470f116
--- /dev/null
+++ b/res/layout/aizy_popup_window.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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:orientation="vertical"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:background="@drawable/aizy_bottom"
+    >
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/caption"
+        android:width="75dip"
+        android:textSize="50sp"
+        android:paddingLeft="20dip"
+        android:gravity="center" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/src/com/android/contacts/list/ContactEntryListFragment.java b/src/com/android/contacts/list/ContactEntryListFragment.java
index 9f398c0..0da9a36 100644
--- a/src/com/android/contacts/list/ContactEntryListFragment.java
+++ b/src/com/android/contacts/list/ContactEntryListFragment.java
@@ -91,11 +91,13 @@
     private boolean mPhotoLoaderEnabled;
     private boolean mSearchMode;
     private boolean mSearchResultsMode;
+    private boolean mAizyEnabled;
     private String mQueryString;
 
     private T mAdapter;
     private View mView;
     private ListView mListView;
+    private ContactListAizyView mAizy;
 
     /**
      * Used for keeping track of the scroll state of the list.
@@ -306,6 +308,9 @@
             mAdapter.changeCursor(partitionIndex, data);
             showCount(partitionIndex, data);
         }
+        if (partitionIndex == mAdapter.getIndexedPartition()) {
+            mAizy.setIndexer(mAdapter.getIndexer());
+        }
     }
 
     private DirectoryPartition createDirectoryPartition(int partitionIndex, Cursor cursor) {
@@ -399,12 +404,34 @@
         if (mAdapter != null) {
             mAdapter.setSectionHeaderDisplayEnabled(flag);
         }
+        configureAizy();
     }
 
     public boolean isSectionHeaderDisplayEnabled() {
         return mSectionHeaderDisplayEnabled;
     }
 
+    public void setAizyEnabled(boolean flag) {
+        mAizyEnabled = flag;
+        configureAizy();
+    }
+
+    public boolean isAizyEnabled() {
+        return mAizyEnabled;
+    }
+
+    private void configureAizy() {
+        boolean hasAisy = isAizyEnabled() && isSectionHeaderDisplayEnabled();
+
+        if (mListView != null) {
+            mListView.setFastScrollEnabled(!hasAisy);
+            mListView.setVerticalScrollBarEnabled(!hasAisy);
+        }
+        if (mAizy != null) {
+            mAizy.setVisibility(hasAisy ? View.VISIBLE : View.GONE);
+        }
+    }
+
     public void setPhotoLoaderEnabled(boolean flag) {
         mPhotoLoaderEnabled = flag;
         configurePhotoLoader();
@@ -569,6 +596,10 @@
             mListView.setOnCreateContextMenuListener(mContextMenuAdapter);
         }
 
+        mAizy = (ContactListAizyView) mView.findViewById(R.id.contacts_list_aizy);
+        mAizy.setListView(mListView);
+
+        configureAizy();
         configurePhotoLoader();
         configureSearchResultText();
     }
@@ -625,6 +656,9 @@
 
     public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
             int totalItemCount) {
+        if (isAizyEnabled()) {
+            mAizy.listOnScroll(firstVisibleItem);
+        }
     }
 
     public void onScrollStateChanged(AbsListView view, int scrollState) {
diff --git a/src/com/android/contacts/list/ContactListAizyView.java b/src/com/android/contacts/list/ContactListAizyView.java
new file mode 100644
index 0000000..07ce12c
--- /dev/null
+++ b/src/com/android/contacts/list/ContactListAizyView.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2010 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.contacts.list;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.PopupWindow;
+import android.widget.SectionIndexer;
+import android.widget.TextView;
+
+/**
+ * A View that displays the sections given by an Indexer and their relative sizes. For
+ * English and similar languages, this is an A to Z list (where only the used letters are
+ * displayed). As the sections are shown in their relative sizes, this View can be used as a
+ * scrollbar.
+ */
+public class ContactListAizyView extends View {
+    private static final String TAG = "ContactListAizyView";
+
+    // TODO: Put these into resource files or create from image resources
+    private static final int TEXT_WIDTH = 20;
+    private static final int CIRCLE_DIAMETER = 30;
+    private static final int PREVIEW_WIDTH = 130;
+    private static final int PREVIEW_HEIGHT = 115;
+
+    private SectionIndexer mIndexer;
+
+    private boolean mCalculateYCoordinates;
+    private ListView mListView;
+    private float mPosition;
+    private float mFactor;
+    private PopupWindow mPreviewPopupWindow;
+    private TextView mPreviewPopupTextView;
+    private boolean mPreviewPopupVisible;
+    private int[] mWindowOffset;
+    private float[] yPositions = null;
+
+    public ContactListAizyView(Context context) {
+        super(context);
+    }
+
+    public ContactListAizyView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public ContactListAizyView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        final LayoutInflater inflater =
+                (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mPreviewPopupWindow = new PopupWindow(
+                inflater.inflate(R.layout.aizy_popup_window, null, false),
+                PREVIEW_WIDTH, PREVIEW_HEIGHT);
+        mPreviewPopupTextView =
+                (TextView) mPreviewPopupWindow.getContentView().findViewById(R.id.caption);
+    }
+
+    public void setIndexer(SectionIndexer indexer) {
+        mIndexer = indexer;
+        mCalculateYCoordinates = true;
+    }
+
+    public void setListView(ListView listView) {
+        mListView = listView;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        setMeasuredDimension(TEXT_WIDTH + CIRCLE_DIAMETER, resolveSize(0, heightMeasureSpec));
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        mCalculateYCoordinates = true;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (mIndexer == null) return;
+
+        calcYCoordinates();
+
+        drawLineAndText(canvas);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (mWindowOffset == null) {
+            mWindowOffset = new int[2];
+            getLocationInWindow(mWindowOffset);
+        }
+
+        final int previewX = mWindowOffset[0] + getWidth();
+        final int previewY = (int) event.getY() + mWindowOffset[1]
+                - mPreviewPopupWindow.getHeight() / 2;
+        final boolean previewPopupVisible = event.getActionMasked() == MotionEvent.ACTION_MOVE;
+        if (previewPopupVisible != mPreviewPopupVisible) {
+            if (previewPopupVisible) {
+                mPreviewPopupWindow.showAtLocation(this, Gravity.LEFT | Gravity.TOP,
+                        previewX, previewY);
+            } else {
+                mPreviewPopupWindow.dismiss();
+            }
+            mPreviewPopupVisible = previewPopupVisible;
+        } else {
+            mPreviewPopupWindow.update(previewX, previewY, -1, -1);
+        }
+        final int position = Math.max(0, (int) (event.getY() / mFactor));
+        if (mIndexer != null) {
+            final int index = mIndexer.getSectionForPosition(position);
+            final Object[] sections = mIndexer.getSections();
+            final String caption =
+                    (index != -1 && index < sections.length) ? sections[index].toString() : "";
+            mPreviewPopupTextView.setText(caption);
+        }
+        if (mListView != null) {
+            mListView.setSelectionFromTop(position, 0);
+        }
+
+        super.onTouchEvent(event);
+        return true;
+    }
+
+    private void calcYCoordinates() {
+        if (!mCalculateYCoordinates) return;
+        mCalculateYCoordinates = false;
+
+        // Get a String[] of the sections.
+        final Object[] sectionObjects = mIndexer.getSections();
+        final int sectionCount = sectionObjects.length;
+        final String[] sections;
+        if (sectionObjects instanceof String[]) {
+            sections = (String[]) sectionObjects;
+        } else {
+            sections = new String[sectionCount];
+            for (int i = 0; i < sectionCount; i++) {
+                sections[i] = sectionObjects[i] == null ? null : sectionObjects[i].toString();
+            }
+        }
+
+        mFactor = (float) getHeight() / mListView.getCount();
+    }
+
+    private void drawLineAndText(Canvas canvas) {
+        // TODO: Figure out how to set the text size and fetch the height in pixels. This
+        // behaviour is OK for prototypes, but has to be refined later
+        final float textSize = 20.0f;
+
+        // Move A down, Z up
+        final Paint paint = new Paint();
+        paint.setColor(Color.LTGRAY);
+        paint.setTextSize(textSize);
+        paint.setAntiAlias(true);
+        final Object[] sections = mIndexer.getSections();
+        canvas.drawLine(
+                TEXT_WIDTH + CIRCLE_DIAMETER * 0.5f, 0.0f,
+                TEXT_WIDTH + CIRCLE_DIAMETER * 0.5f, getHeight(),
+                paint);
+        final int sectionCount = sections.length;
+        if (yPositions == null || yPositions.length != sectionCount) {
+            yPositions = new float[sectionCount];
+        }
+
+        // Calculate Positions
+        for (int i = 0; i < sectionCount; i++) {
+            yPositions[i] = mIndexer.getPositionForSection(i) * mFactor;
+        }
+
+        // Draw
+        float lastVisibleY = Float.MAX_VALUE;
+        for (int i = sectionCount - 1; i >= 0; i--) {
+            final float y = yPositions[i];
+            if (lastVisibleY - textSize > y) {
+                canvas.drawText(sections[i].toString(), 0.0f, y + 0.5f * textSize, paint);
+                lastVisibleY = y;
+            }
+            canvas.drawLine(
+                    TEXT_WIDTH + CIRCLE_DIAMETER * 0.5f - 2, y,
+                    TEXT_WIDTH + CIRCLE_DIAMETER * 0.5f + 2, y,
+                    paint);
+        }
+
+        paint.setColor(Color.YELLOW);
+        canvas.drawLine(
+                TEXT_WIDTH + CIRCLE_DIAMETER * 0.0f, mPosition * mFactor,
+                TEXT_WIDTH + CIRCLE_DIAMETER * 1.0f, mPosition * mFactor,
+                paint);
+    }
+
+    public void listOnScroll(int firstVisibleItem) {
+        mPosition = firstVisibleItem;
+        invalidate();
+    }
+}
diff --git a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
index 13361e3..6c5559f 100644
--- a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
+++ b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
@@ -43,6 +43,7 @@
     public DefaultContactBrowseListFragment() {
         setPhotoLoaderEnabled(true);
         setSectionHeaderDisplayEnabled(true);
+        setAizyEnabled(true);
     }
 
     @Override
diff --git a/src/com/android/contacts/widget/IndexerListAdapter.java b/src/com/android/contacts/widget/IndexerListAdapter.java
index 35ce9a6..c39e2f2 100644
--- a/src/com/android/contacts/widget/IndexerListAdapter.java
+++ b/src/com/android/contacts/widget/IndexerListAdapter.java
@@ -68,6 +68,10 @@
         this.mIndexedPartition = partition;
     }
 
+    public SectionIndexer getIndexer() {
+        return mIndexer;
+    }
+
     public void setIndexer(SectionIndexer indexer) {
         mIndexer = indexer;
     }