From 179ada958b0bb46c6b9c8eb8b220d84dd3db855a Mon Sep 17 00:00:00 2001
From: "Tadashi G. Takaoka" <takaoka@google.com>
Date: Wed, 29 Sep 2010 14:30:01 +0900
Subject: [PATCH] Refactor CandidateView touch event handling

This change also fixes tha the touch slop value is applyed only for
initial movement of scrolling suggestion bar.

Bug: 3004920
Change-Id: I62afdedc210156e41e8c84c48cade442f9d5a1aa
---
 java/res/values/dimens.xml                    |   1 +
 .../inputmethod/latin/CandidateView.java      | 186 +++++++-----------
 2 files changed, 77 insertions(+), 110 deletions(-)

diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml
index 502157dcf..f83fc36a6 100644
--- a/java/res/values/dimens.xml
+++ b/java/res/values/dimens.xml
@@ -33,4 +33,5 @@
     <dimen name="mini_keyboard_slide_allowance">91.8dip</dimen>
     <!-- -key_height x 1.0 -->
     <dimen name="mini_keyboard_vertical_correction">-54dip</dimen>
+    <dimen name="candidate_min_touchable_width">0.3in</dimen>
 </resources>
diff --git a/java/src/com/android/inputmethod/latin/CandidateView.java b/java/src/com/android/inputmethod/latin/CandidateView.java
index 0f5b43009..9bbd30e24 100755
--- a/java/src/com/android/inputmethod/latin/CandidateView.java
+++ b/java/src/com/android/inputmethod/latin/CandidateView.java
@@ -16,17 +16,13 @@
 
 package com.android.inputmethod.latin;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Paint;
+import android.graphics.Paint.Align;
 import android.graphics.Rect;
 import android.graphics.Typeface;
-import android.graphics.Paint.Align;
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
 import android.os.Message;
@@ -40,75 +36,82 @@ import android.view.ViewGroup.LayoutParams;
 import android.widget.PopupWindow;
 import android.widget.TextView;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 public class CandidateView extends View {
 
     private static final int OUT_OF_BOUNDS = -1;
-    private static final List<CharSequence> EMPTY_LIST = new ArrayList<CharSequence>();
 
     private LatinIME mService;
-    private List<CharSequence> mSuggestions = EMPTY_LIST;
+    private final ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>();
     private boolean mShowingCompletions;
     private CharSequence mSelectedString;
     private int mSelectedIndex;
     private int mTouchX = OUT_OF_BOUNDS;
-    private Drawable mSelectionHighlight;
+    private final Drawable mSelectionHighlight;
     private boolean mTypedWordValid;
     
     private boolean mHaveMinimalSuggestion;
     
     private Rect mBgPadding;
 
-    private TextView mPreviewText;
-    private PopupWindow mPreviewPopup;
+    private final TextView mPreviewText;
+    private final PopupWindow mPreviewPopup;
+    private final int mDelayAfterPreview;
     private int mCurrentWordIndex;
     private Drawable mDivider;
     
     private static final int MAX_SUGGESTIONS = 32;
     private static final int SCROLL_PIXELS = 20;
     
-    private static final int MSG_REMOVE_PREVIEW = 1;
-    private static final int MSG_REMOVE_THROUGH_PREVIEW = 2;
-    
-    private int[] mWordWidth = new int[MAX_SUGGESTIONS];
-    private int[] mWordX = new int[MAX_SUGGESTIONS];
+    private final int[] mWordWidth = new int[MAX_SUGGESTIONS];
+    private final int[] mWordX = new int[MAX_SUGGESTIONS];
     private int mPopupPreviewX;
     private int mPopupPreviewY;
 
     private static final int X_GAP = 10;
     
-    private int mColorNormal;
-    private int mColorRecommended;
-    private int mColorOther;
-    private Paint mPaint;
-    private int mDescent;
+    private final int mColorNormal;
+    private final int mColorRecommended;
+    private final int mColorOther;
+    private final Paint mPaint;
+    private final int mDescent;
     private boolean mScrolled;
     private boolean mShowingAddToDictionary;
     private CharSequence mAddToDictionaryHint;
 
     private int mTargetScrollX;
 
-    private int mMinTouchableWidth;
+    private final int mMinTouchableWidth;
 
     private int mTotalWidth;
     
-    private GestureDetector mGestureDetector;
+    private final GestureDetector mGestureDetector;
+
+    private final UIHandler mHandler = new UIHandler();
+
+    private class UIHandler extends Handler {
+        private static final int MSG_DISMISS_PREVIEW = 1;
 
-    Handler mHandler = new Handler() {
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
-                case MSG_REMOVE_PREVIEW:
-                    mPreviewText.setVisibility(GONE);
-                    break;
-                case MSG_REMOVE_THROUGH_PREVIEW:
-                    mPreviewText.setVisibility(GONE);
-                    if (mTouchX != OUT_OF_BOUNDS) {
-                        removeHighlight();
-                    }
+                case MSG_DISMISS_PREVIEW:
+                    mPreviewPopup.dismiss();
                     break;
             }
         }
-    };
+
+        public void dismissPreview(long delay) {
+            sendMessageDelayed(obtainMessage(MSG_DISMISS_PREVIEW), delay);
+        }
+
+        public void cancelDismissPreview() {
+            removeMessages(MSG_DISMISS_PREVIEW);
+        }
+    }
 
     /**
      * Construct a CandidateView for showing suggested words for completion.
@@ -129,6 +132,8 @@ public class CandidateView extends View {
         mPreviewPopup.setWindowLayoutMode(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
         mPreviewPopup.setContentView(mPreviewText);
         mPreviewPopup.setBackgroundDrawable(null);
+        mPreviewPopup.setAnimationStyle(R.style.KeyPreviewAnimation);
+        mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview);
         mColorNormal = res.getColor(R.color.candidate_normal);
         mColorRecommended = res.getColor(R.color.candidate_recommended);
         mColorOther = res.getColor(R.color.candidate_other);
@@ -142,13 +147,10 @@ public class CandidateView extends View {
         mPaint.setStrokeWidth(0);
         mPaint.setTextAlign(Align.CENTER);
         mDescent = (int) mPaint.descent();
-        // 50 pixels for a 160dpi device would mean about 0.3 inch
-        mMinTouchableWidth = (int) (getResources().getDisplayMetrics().density * 50);
+        mMinTouchableWidth = (int)res.getDimension(R.dimen.candidate_min_touchable_width);
         
         // Slightly reluctant to scroll to be able to easily choose the suggestion
-        // 50 pixels for a 160dpi device would mean about 0.3 inch
-        final int touchSlop = (int) (getResources().getDisplayMetrics().density * 50);
-        final int touchSlopSquare = touchSlop * touchSlop;
+        final int touchSlopSquare = mMinTouchableWidth * mMinTouchableWidth;
         mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
             @Override
             public void onLongPress(MotionEvent me) {
@@ -158,15 +160,25 @@ public class CandidateView extends View {
                     }
                 }
             }
-            
+
+            @Override
+            public boolean onDown(MotionEvent e) {
+                mScrolled = false;
+                return false;
+            }
+
             @Override
             public boolean onScroll(MotionEvent e1, MotionEvent e2,
                     float distanceX, float distanceY) {
-                final int deltaX = (int) (e2.getX() - e1.getX());
-                final int deltaY = (int) (e2.getY() - e1.getY());
-                final int distance = (deltaX * deltaX) + (deltaY * deltaY);
-                if (distance < touchSlopSquare) {
-                    return false;
+                if (!mScrolled) {
+                    // This is applied only when we recognize that scrolling is starting.
+                    final int deltaX = (int) (e2.getX() - e1.getX());
+                    final int deltaY = (int) (e2.getY() - e1.getY());
+                    final int distance = (deltaX * deltaX) + (deltaY * deltaY);
+                    if (distance < touchSlopSquare) {
+                        return true;
+                    }
+                    mScrolled = true;
                 }
 
                 final int width = getWidth();
@@ -215,7 +227,6 @@ public class CandidateView extends View {
             super.onDraw(canvas);
         }
         mTotalWidth = 0;
-        if (mSuggestions == null) return;
         
         final int height = getHeight();
         if (mBgPadding == null) {
@@ -226,8 +237,8 @@ public class CandidateView extends View {
             mDivider.setBounds(0, 0, mDivider.getIntrinsicWidth(),
                     mDivider.getIntrinsicHeight());
         }
-        int x = 0;
-        final int count = Math.min(mSuggestions.size(), MAX_SUGGESTIONS);
+
+        final int count = mSuggestions.size();
         final Rect bgPadding = mBgPadding;
         final Paint paint = mPaint;
         final int touchX = mTouchX;
@@ -238,25 +249,26 @@ public class CandidateView extends View {
 
         boolean existsAutoCompletion = false;
 
+        int x = 0;
         for (int i = 0; i < count; i++) {
             CharSequence suggestion = mSuggestions.get(i);
             if (suggestion == null) continue;
+            final int wordLength = suggestion.length();
+
             paint.setColor(mColorNormal);
             if (mHaveMinimalSuggestion 
                     && ((i == 1 && !typedWordValid) || (i == 0 && typedWordValid))) {
                 paint.setTypeface(Typeface.DEFAULT_BOLD);
                 paint.setColor(mColorRecommended);
                 existsAutoCompletion = true;
-            } else if (i != 0 || (suggestion.length() == 1 && count > 1)) {
+            } else if (i != 0 || (wordLength == 1 && count > 1)) {
                 // HACK: even if i == 0, we use mColorOther when this suggestion's length is 1 and
                 // there are multiple suggestions, such as the default punctuation list.
                 paint.setColor(mColorOther);
             }
-            final int wordWidth;
-            if (mWordWidth[i] != 0) {
-                wordWidth = mWordWidth[i];
-            } else {
-                float textWidth =  paint.measureText(suggestion, 0, suggestion.length());
+            int wordWidth;
+            if ((wordWidth = mWordWidth[i]) == 0) {
+                float textWidth =  paint.measureText(suggestion, 0, wordLength);
                 wordWidth = Math.max(mMinTouchableWidth, (int) textWidth + X_GAP * 2);
                 mWordWidth[i] = wordWidth;
             }
@@ -277,7 +289,7 @@ public class CandidateView extends View {
             }
 
             if (canvas != null) {
-                canvas.drawText(suggestion, 0, suggestion.length(), x + wordWidth / 2, y, paint);
+                canvas.drawText(suggestion, 0, wordLength, x + wordWidth / 2, y, paint);
                 paint.setColor(mColorOther);
                 canvas.translate(x + wordWidth, 0);
                 // Draw a divider unless it's after the hint
@@ -324,7 +336,12 @@ public class CandidateView extends View {
             boolean typedWordValid, boolean haveMinimalSuggestion) {
         clear();
         if (suggestions != null) {
-            mSuggestions = new ArrayList<CharSequence>(suggestions);
+            int insertCount = Math.min(suggestions.size(), MAX_SUGGESTIONS);
+            for (CharSequence suggestion : suggestions) {
+                mSuggestions.add(suggestion);
+                if (--insertCount == 0)
+                    break;
+            }
         }
         mShowingCompletions = completions;
         mTypedWordValid = typedWordValid;
@@ -355,50 +372,6 @@ public class CandidateView extends View {
         return true;
     }
 
-    public void scrollPrev() {
-        int i = 0;
-        final int count = Math.min(mSuggestions.size(), MAX_SUGGESTIONS);
-        int firstItem = 0; // Actually just before the first item, if at the boundary
-        while (i < count) {
-            if (mWordX[i] < getScrollX() 
-                    && mWordX[i] + mWordWidth[i] >= getScrollX() - 1) {
-                firstItem = i;
-                break;
-            }
-            i++;
-        }
-        int leftEdge = mWordX[firstItem] + mWordWidth[firstItem] - getWidth();
-        if (leftEdge < 0) leftEdge = 0;
-        updateScrollPosition(leftEdge);
-    }
-    
-    public void scrollNext() {
-        int i = 0;
-        int scrollX = getScrollX();
-        int targetX = scrollX;
-        final int count = Math.min(mSuggestions.size(), MAX_SUGGESTIONS);
-        int rightEdge = scrollX + getWidth();
-        while (i < count) {
-            if (mWordX[i] <= rightEdge &&
-                    mWordX[i] + mWordWidth[i] >= rightEdge) {
-                targetX = Math.min(mWordX[i], mTotalWidth - getWidth());
-                break;
-            }
-            i++;
-        }
-        updateScrollPosition(targetX);
-    }
-
-    private void updateScrollPosition(int targetX) {
-        if (targetX != getScrollX()) {
-            // TODO: Animate
-            mTargetScrollX = targetX;
-            requestLayout();
-            invalidate();
-            mScrolled = true;
-        }
-    }
-
     /* package */ List<CharSequence> getSuggestions() {
         return mSuggestions;
     }
@@ -406,7 +379,7 @@ public class CandidateView extends View {
     public void clear() {
         // Don't call mSuggestions.clear() because it's being used for logging
         // in LatinIME.pickSuggestionManually().
-        mSuggestions = EMPTY_LIST;
+        mSuggestions.clear();
         mTouchX = OUT_OF_BOUNDS;
         mSelectedString = null;
         mSelectedIndex = -1;
@@ -414,9 +387,7 @@ public class CandidateView extends View {
         invalidate();
         Arrays.fill(mWordWidth, 0);
         Arrays.fill(mWordX, 0);
-        if (mPreviewPopup.isShowing()) {
-            mPreviewPopup.dismiss();
-        }
+        mPreviewPopup.dismiss();
     }
     
     @Override
@@ -433,7 +404,6 @@ public class CandidateView extends View {
 
         switch (action) {
         case MotionEvent.ACTION_DOWN:
-            mScrolled = false;
             invalidate();
             break;
         case MotionEvent.ACTION_MOVE:
@@ -453,7 +423,6 @@ public class CandidateView extends View {
                     mSelectedIndex = -1;
                 }
             }
-            invalidate();
             break;
         case MotionEvent.ACTION_UP:
             if (!mScrolled) {
@@ -473,8 +442,8 @@ public class CandidateView extends View {
             mSelectedString = null;
             mSelectedIndex = -1;
             removeHighlight();
-            hidePreview();
             requestLayout();
+            mHandler.dismissPreview(mDelayAfterPreview);
             break;
         }
         return true;
@@ -482,10 +451,7 @@ public class CandidateView extends View {
 
     private void hidePreview() {
         mCurrentWordIndex = OUT_OF_BOUNDS;
-        if (mPreviewPopup.isShowing()) {
-            mHandler.sendMessageDelayed(mHandler
-                    .obtainMessage(MSG_REMOVE_PREVIEW), 60);
-        }
+        mPreviewPopup.dismiss();
     }
     
     private void showPreview(int wordIndex, String altText) {
@@ -496,6 +462,7 @@ public class CandidateView extends View {
             if (wordIndex == OUT_OF_BOUNDS) {
                 hidePreview();
             } else {
+                mHandler.cancelDismissPreview();
                 CharSequence word = altText != null? altText : mSuggestions.get(wordIndex);
                 mPreviewText.setText(word);
                 mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 
@@ -508,7 +475,6 @@ public class CandidateView extends View {
                 mPopupPreviewX = mWordX[wordIndex] - mPreviewText.getPaddingLeft() - getScrollX()
                         + (mWordWidth[wordIndex] - wordWidth) / 2;
                 mPopupPreviewY = - popupHeight;
-                mHandler.removeMessages(MSG_REMOVE_PREVIEW);
                 int [] offsetInWindow = new int[2];
                 getLocationInWindow(offsetInWindow);
                 if (mPreviewPopup.isShowing()) {