Implement expandable candidates pane

This change removes horizontal scroll from candidates strip. Instead
of that this change introduces "fixed 3 items candidates strip" and
"expandable candidates pane".

Bug: 4175031

Change-Id: Ia367d9074436fdea76d3b653d81798ce2749170e
diff --git a/java/res/drawable-hdpi/btn_close_candidates_pane.9.png b/java/res/drawable-hdpi/btn_close_candidates_pane.9.png
new file mode 100644
index 0000000..6df00f2
--- /dev/null
+++ b/java/res/drawable-hdpi/btn_close_candidates_pane.9.png
Binary files differ
diff --git a/java/res/drawable-hdpi/btn_expand_candidates_pane.9.png b/java/res/drawable-hdpi/btn_expand_candidates_pane.9.png
new file mode 100644
index 0000000..63015ec
--- /dev/null
+++ b/java/res/drawable-hdpi/btn_expand_candidates_pane.9.png
Binary files differ
diff --git a/java/res/drawable-mdpi/btn_close_candidates_pane.9.png b/java/res/drawable-mdpi/btn_close_candidates_pane.9.png
new file mode 100644
index 0000000..5ea5692
--- /dev/null
+++ b/java/res/drawable-mdpi/btn_close_candidates_pane.9.png
Binary files differ
diff --git a/java/res/drawable-mdpi/btn_expand_candidates_pane.9.png b/java/res/drawable-mdpi/btn_expand_candidates_pane.9.png
new file mode 100644
index 0000000..83cb653
--- /dev/null
+++ b/java/res/drawable-mdpi/btn_expand_candidates_pane.9.png
Binary files differ
diff --git a/java/res/layout/candidates_strip.xml b/java/res/layout/candidates_strip.xml
new file mode 100644
index 0000000..296ea75
--- /dev/null
+++ b/java/res/layout/candidates_strip.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2011, 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.
+*/
+-->
+
+<merge
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+>
+    <include
+        android:id="@+id/candidate_left"
+        layout="@layout/candidate" />
+    <include
+        layout="@layout/candidate_divider" />
+    <include
+        android:id="@+id/candidate_center"
+        layout="@layout/candidate" />
+    <include
+        layout="@layout/candidate_divider" />
+    <LinearLayout
+        android:orientation="horizontal"
+        android:layout_weight="1.0"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:gravity="center_vertical"
+    >
+        <include
+            android:id="@+id/candidate_right"
+            layout="@layout/candidate" />
+        <!-- TODO: These images' drawable must be determined depending on theme. -->
+        <ImageButton
+            android:id="@+id/expand_candidates_pane"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/btn_expand_candidates_pane"
+            android:visibility="gone"
+            style="?attr/suggestionBackgroundStyle" />
+        <ImageButton
+            android:id="@+id/close_candidates_pane"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/btn_close_candidates_pane"
+            android:visibility="gone"
+            style="?attr/suggestionBackgroundStyle" />
+    </LinearLayout>
+</merge>
diff --git a/java/res/layout/input_view.xml b/java/res/layout/input_view.xml
index 5da1a48..52b5ecc 100644
--- a/java/res/layout/input_view.xml
+++ b/java/res/layout/input_view.xml
@@ -32,32 +32,47 @@
         android:id="@+id/candidates_container"
         android:orientation="horizontal"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        style="?attr/suggestionsStripBackgroundStyle"
+        android:layout_height="@dimen/candidate_strip_minimum_height"
+        android:gravity="bottom"
     >
         <View
             android:layout_width="@dimen/candidate_strip_padding"
             android:layout_height="@dimen/candidate_strip_height"
             style="?attr/suggestionsStripBackgroundStyle" />
-        <HorizontalScrollView
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:fadingEdge="horizontal"
-            android:fadingEdgeLength="@dimen/candidate_strip_fading_edge_length"
-            android:scrollbars="none"
-        >
-            <com.android.inputmethod.latin.CandidateView
-                android:id="@+id/candidates"
-                android:layout_width="match_parent"
-                android:layout_height="@dimen/candidate_strip_height"
-                android:gravity="center_vertical" />
-        </HorizontalScrollView>
+        <com.android.inputmethod.latin.CandidateView
+            android:id="@+id/candidates"
+            android:layout_weight="1.0"
+            android:layout_width="0dp"
+            android:layout_height="@dimen/candidate_strip_height"
+            android:gravity="center_vertical"
+            style="?attr/suggestionsStripBackgroundStyle" />
         <View
             android:layout_width="@dimen/candidate_strip_padding"
             android:layout_height="@dimen/candidate_strip_height"
             style="?attr/suggestionsStripBackgroundStyle" />
     </LinearLayout>
 
+    <LinearLayout
+        android:id="@+id/candidates_pane_container"
+        android:orientation="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        style="?attr/suggestionsStripBackgroundStyle"
+    >
+        <View
+            android:layout_width="@dimen/candidate_strip_padding"
+            android:layout_height="@dimen/candidate_strip_height" />
+        <FrameLayout
+            android:id="@+id/candidates_pane"
+            android:layout_weight="1.0"
+            android:layout_width="0dp"
+            android:layout_height="match_parent" />
+        <View
+            android:layout_width="@dimen/candidate_strip_padding"
+            android:layout_height="@dimen/candidate_strip_height" />
+    </LinearLayout>
+
     <com.android.inputmethod.keyboard.LatinKeyboardView
         android:id="@+id/keyboard_view"
         android:layout_alignParentBottom="true"
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 7c68c93..8e9c7ef 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -57,6 +57,7 @@
     private SubtypeSwitcher mSubtypeSwitcher;
     private SharedPreferences mPrefs;
 
+    private View mCurrentInputView;
     private LatinKeyboardView mKeyboardView;
     private LatinIME mInputMethodService;
 
@@ -294,7 +295,7 @@
     }
 
     public boolean isInputViewShown() {
-        return mKeyboardView != null && mKeyboardView.isShown();
+        return mCurrentInputView != null && mCurrentInputView.isShown();
     }
 
     public boolean isKeyboardAvailable() {
@@ -714,9 +715,6 @@
         return createInputView(mThemeIndex, true);
     }
 
-    // Instance variable only for {@link #createInputView(int, boolean)}.
-    private View mCurrentInputView;
-
     private View createInputView(final int newThemeIndex, final boolean forceRecreate) {
         if (mCurrentInputView != null && mThemeIndex == newThemeIndex && !forceRecreate)
             return mCurrentInputView;
diff --git a/java/src/com/android/inputmethod/latin/CandidateView.java b/java/src/com/android/inputmethod/latin/CandidateView.java
index b4f6b2c..5ef1d75 100644
--- a/java/src/com/android/inputmethod/latin/CandidateView.java
+++ b/java/src/com/android/inputmethod/latin/CandidateView.java
@@ -56,13 +56,23 @@
 
     private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD);
     private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan();
-    private static final int MAX_SUGGESTIONS = 16;
+    // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}.
+    private static final int MAX_SUGGESTIONS = 18;
+    private static final int UNSPECIFIED_MEASURESPEC = MeasureSpec.makeMeasureSpec(
+            0, MeasureSpec.UNSPECIFIED);
 
     private static final boolean DBG = LatinImeLogger.sDBG;
 
+    private static final int NUM_CANDIDATES_IN_STRIP = 3;
+    private final View mExpandCandidatesPane;
+    private final View mCloseCandidatesPane;
+    private ViewGroup mCandidatesPane;
+    private ViewGroup mCandidatesPaneContainer;
+    private View mKeyboardView;
     private final ArrayList<TextView> mWords = new ArrayList<TextView>();
     private final ArrayList<View> mDividers = new ArrayList<View>();
     private final int mCandidatePadding;
+    private final int mCandidateStripHeight;
     private final boolean mConfigCandidateHighlightFontColorEnabled;
     private final CharacterStyle mInvertedForegroundColorSpan;
     private final CharacterStyle mInvertedBackgroundColorSpan;
@@ -132,8 +142,10 @@
         super(context, attrs);
 
         Resources res = context.getResources();
-        mPreviewPopup = new PopupWindow(context);
         LayoutInflater inflater = LayoutInflater.from(context);
+        inflater.inflate(R.layout.candidates_strip, this);
+
+        mPreviewPopup = new PopupWindow(context);
         mPreviewText = (TextView) inflater.inflate(R.layout.candidate_preview, null);
         mPreviewPopup.setWindowLayoutMode(ViewGroup.LayoutParams.WRAP_CONTENT,
                 ViewGroup.LayoutParams.WRAP_CONTENT);
@@ -148,8 +160,26 @@
         mInvertedBackgroundColorSpan = new BackgroundColorSpan(mColorTypedWord);
 
         mCandidatePadding = res.getDimensionPixelOffset(R.dimen.candidate_padding);
+        mCandidateStripHeight = res.getDimensionPixelOffset(R.dimen.candidate_strip_height);
         for (int i = 0; i < MAX_SUGGESTIONS; i++) {
-            final TextView tv = (TextView)inflater.inflate(R.layout.candidate, null);
+            final TextView tv;
+            switch (i) {
+            case 0:
+                tv = (TextView)findViewById(R.id.candidate_left);
+                tv.setPadding(mCandidatePadding, 0, 0, 0);
+                break;
+            case 1:
+                tv = (TextView)findViewById(R.id.candidate_center);
+                break;
+            case 2:
+                tv = (TextView)findViewById(R.id.candidate_right);
+                break;
+            default:
+                tv = (TextView)inflater.inflate(R.layout.candidate, null);
+                break;
+            }
+            if (i < NUM_CANDIDATES_IN_STRIP)
+                setLayoutWeight(tv, 1.0f);
             tv.setTag(i);
             tv.setOnClickListener(this);
             if (i == 0)
@@ -157,19 +187,38 @@
             mWords.add(tv);
             if (i > 0) {
                 final View divider = inflater.inflate(R.layout.candidate_divider, null);
+                divider.measure(UNSPECIFIED_MEASURESPEC, UNSPECIFIED_MEASURESPEC);
                 mDividers.add(divider);
             }
         }
 
-        scrollTo(0, getScrollY());
+        mExpandCandidatesPane = findViewById(R.id.expand_candidates_pane);
+        mExpandCandidatesPane.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                expandCandidatesPane();
+            }
+        });
+        mCloseCandidatesPane = findViewById(R.id.close_candidates_pane);
+        mCloseCandidatesPane.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                closeCandidatesPane();
+            }
+        });
     }
 
     /**
      * A connection back to the input method.
      * @param listener
      */
-    public void setListener(Listener listener) {
+    public void setListener(Listener listener, View inputView) {
         mListener = listener;
+        mKeyboardView = inputView.findViewById(R.id.keyboard_view);
+        mCandidatesPane = (ViewGroup)inputView.findViewById(R.id.candidates_pane);
+        mCandidatesPane.setOnClickListener(this);
+        mCandidatesPaneContainer = (ViewGroup)inputView.findViewById(
+                R.id.candidates_pane_container);
     }
 
     public void setSuggestions(SuggestedWords suggestions) {
@@ -183,6 +232,15 @@
         }
     }
 
+    private static void setLayoutWeight(View v, float weight) {
+        ViewGroup.LayoutParams lp = v.getLayoutParams();
+        if (lp instanceof LinearLayout.LayoutParams) {
+            LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp;
+            llp.width = 0;
+            llp.weight = weight;
+        }
+    }
+
     private CharSequence getStyledCandidateWord(CharSequence word, boolean isAutoCorrect) {
         if (!isAutoCorrect)
             return word;
@@ -216,7 +274,14 @@
         final List<SuggestedWordInfo> suggestedWordInfoList = suggestions.mSuggestedWordInfoList;
 
         clear();
+        final int paneWidth = getWidth();
+        final int dividerWidth = mDividers.get(0).getMeasuredWidth();
+        int x = 0;
+        int y = 0;
+        int fromIndex = NUM_CANDIDATES_IN_STRIP;
         final int count = Math.min(mWords.size(), suggestions.size());
+        closeCandidatesPane();
+        mExpandCandidatesPane.setEnabled(count >= NUM_CANDIDATES_IN_STRIP);
         for (int i = 0; i < count; i++) {
             final CharSequence word = suggestions.getWord(i);
             if (word == null) continue;
@@ -233,33 +298,91 @@
             final boolean isPunctuationSuggestions = (word.length() == 1 && count > 1);
 
             final TextView tv = mWords.get(i);
+            // TODO: Reorder candidates in strip as appropriate. The center candidate should hold
+            // the word when space is typed (valid typed word or auto corrected word).
             tv.setTextColor(getCandidateTextColor(isAutoCorrect,
                     isSuggestedCandidate || isPunctuationSuggestions, info));
             tv.setText(getStyledCandidateWord(word, isAutoCorrect));
-            if (i == 0) {
-                tv.setPadding(mCandidatePadding, 0, 0, 0);
-            } else if (i == count - 1) {
-                tv.setPadding(0, 0, mCandidatePadding, 0);
-            } else {
-                tv.setPadding(0, 0, 0, 0);
+            // TODO: call TextView.setTextScaleX() to fit the candidate in single line.
+            if (i >= NUM_CANDIDATES_IN_STRIP) {
+                tv.measure(UNSPECIFIED_MEASURESPEC, UNSPECIFIED_MEASURESPEC);
+                final int width = tv.getMeasuredWidth();
+                // TODO: Handle overflow case.
+                if (dividerWidth + x + width >= paneWidth) {
+                    centeringCandidates(fromIndex, i - 1, x, paneWidth);
+                    x = 0;
+                    y += mCandidateStripHeight;
+                    fromIndex = i;
+                }
+                if (x != 0) {
+                    final View divider = mDividers.get(i - NUM_CANDIDATES_IN_STRIP);
+                    mCandidatesPane.addView(divider);
+                    placeCandidateAt(divider, x, y);
+                    x += dividerWidth;
+                }
+                mCandidatesPane.addView(tv);
+                placeCandidateAt(tv, x, y);
+                x += width;
             }
-            if (i > 0)
-                addView(mDividers.get(i - 1));
-            addView(tv);
 
             if (DBG && info != null) {
                 final TextView dv = new TextView(getContext(), null);
                 dv.setTextSize(10.0f);
                 dv.setTextColor(0xff808080);
                 dv.setText(info.getDebugString());
-                addView(dv);
+                // TODO: debug view for candidate strip needed.
+                mCandidatesPane.addView(dv);
                 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)dv.getLayoutParams();
                 lp.gravity = Gravity.BOTTOM;
             }
         }
+        if (x != 0) {
+            // Centering last candidates row.
+            centeringCandidates(fromIndex, count - 1, x, paneWidth);
+        }
+    }
 
-        scrollTo(0, getScrollY());
-        requestLayout();
+    private void placeCandidateAt(View v, int x, int y) {
+        ViewGroup.LayoutParams lp = v.getLayoutParams();
+        if (lp instanceof ViewGroup.MarginLayoutParams) {
+            ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams)lp;
+            mlp.width = v.getMeasuredWidth();
+            mlp.height = v.getMeasuredHeight();
+            mlp.setMargins(x, y + (mCandidateStripHeight - mlp.height) / 2, 0, 0);
+        }
+    }
+
+    private void centeringCandidates(int from, int to, int width, int paneWidth) {
+        final ViewGroup pane = mCandidatesPane;
+        final int fromIndex = pane.indexOfChild(mWords.get(from));
+        final int toIndex = pane.indexOfChild(mWords.get(to));
+        final int offset = (paneWidth - width) / 2;
+        for (int index = fromIndex; index <= toIndex; index++) {
+            offsetMargin(pane.getChildAt(index), offset, 0);
+        }
+    }
+
+    private static void offsetMargin(View v, int dx, int dy) {
+        ViewGroup.LayoutParams lp = v.getLayoutParams();
+        if (lp instanceof ViewGroup.MarginLayoutParams) {
+            ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams)lp;
+            mlp.setMargins(mlp.leftMargin + dx, mlp.topMargin + dy, 0, 0);
+        }
+    }
+
+    private void expandCandidatesPane() {
+        mExpandCandidatesPane.setVisibility(View.GONE);
+        mCloseCandidatesPane.setVisibility(View.VISIBLE);
+        mCandidatesPaneContainer.setMinimumHeight(mKeyboardView.getMeasuredHeight());
+        mCandidatesPaneContainer.setVisibility(View.VISIBLE);
+        mKeyboardView.setVisibility(View.GONE);
+    }
+
+    private void closeCandidatesPane() {
+        mExpandCandidatesPane.setVisibility(View.VISIBLE);
+        mCloseCandidatesPane.setVisibility(View.GONE);
+        mCandidatesPaneContainer.setVisibility(View.GONE);
+        mKeyboardView.setVisibility(View.VISIBLE);
     }
 
     public void onAutoCorrectionInverted(CharSequence autoCorrectedWord) {
@@ -310,7 +433,9 @@
     public void clear() {
         mShowingAddToDictionary = false;
         mShowingAutoCorrectionInverted = false;
-        removeAllViews();
+        for (int i = 0; i < NUM_CANDIDATES_IN_STRIP; i++)
+            mWords.get(i).setText(null);
+        mCandidatesPane.removeAllViews();
     }
 
     private void hidePreview() {
@@ -349,9 +474,13 @@
 
     @Override
     public boolean onLongClick(View view) {
-        final int index = (Integer) view.getTag();
+        final Object tag = view.getTag();
+        if (!(tag instanceof Integer))
+            return true;
+        final int index = (Integer) tag;
         if (index >= mSuggestions.size())
             return true;
+
         final CharSequence word = mSuggestions.getWord(index);
         if (word.length() < 2)
             return false;
@@ -361,15 +490,22 @@
 
     @Override
     public void onClick(View view) {
-        final int index = (Integer) view.getTag();
+        final Object tag = view.getTag();
+        if (!(tag instanceof Integer))
+            return;
+        final int index = (Integer) tag;
         if (index >= mSuggestions.size())
             return;
+
         final CharSequence word = mSuggestions.getWord(index);
         if (mShowingAddToDictionary && index == 0) {
             addToDictionary(word);
         } else {
             mListener.pickSuggestionManually(index, word);
         }
+        // Because some punctuation letters are not treated as word separator depending on locale,
+        // {@link #setSuggestions} might not be called and candidates pane left opened.
+        closeCandidatesPane();
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 940f6b8..75bca99 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -502,7 +502,7 @@
         super.setInputView(view);
         mCandidateViewContainer = view.findViewById(R.id.candidates_container);
         mCandidateView = (CandidateView) view.findViewById(R.id.candidates);
-        mCandidateView.setListener(this);
+        mCandidateView.setListener(this, view);
         mCandidateStripHeight = (int)mResources.getDimension(R.dimen.candidate_strip_height);
     }
 
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 33f9820..62788fb 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -84,7 +84,7 @@
     private final Map<String, Dictionary> mUnigramDictionaries = new HashMap<String, Dictionary>();
     private final Map<String, Dictionary> mBigramDictionaries = new HashMap<String, Dictionary>();
 
-    private int mPrefMaxSuggestions = 12;
+    private int mPrefMaxSuggestions = 18;
 
     private static final int PREF_MAX_BIGRAMS = 60;