From a71a244ec6c83e8627f10d3d04228ff8e7915c09 Mon Sep 17 00:00:00 2001 From: Seigo Nonaka Date: Fri, 19 Jun 2015 15:00:43 +0900 Subject: [PATCH] Make suggestion window style material. To make suggestion window style Material, this CL does following things: 1. Introduce LinearLayout to be able to split suggestion item and menu. Currently suggestion menus, "Add to Dictionary" and "Delete" buttons are children of ListView. It is necessary to introduce LinearLayout and move these two menus from ListView to this LinearLayout to have a divider between suggestion items and menus. 2. Extract suggestion window layout definition from Java. Currently almost all layout of suggestion popup window is done by Editor.java. By extracting this logic from Java and move it to XML files, it becomes easy to support both Holo and Material theme. 3. Introduce Material Design. Suggestion window should respect the running application's theme since suggestion window is shown as the part of the application. This patch introduces Material themed suggestion window, and at the same time, the old window is also kept as the Holo themed suggestion window. Bug: 15347319 Change-Id: Ieccea12db95c0a040b38680ae794b1cf6971736f --- core/java/android/widget/Editor.java | 259 +++++++++++---------- core/java/android/widget/TextView.java | 12 +- .../res/layout/text_edit_suggestion_container.xml | 43 ++++ .../text_edit_suggestion_container_material.xml | 42 ++++ core/res/res/layout/text_edit_suggestion_item.xml | 17 +- .../layout/text_edit_suggestion_item_material.xml | 19 ++ core/res/res/values/attrs.xml | 8 + core/res/res/values/styles.xml | 10 +- core/res/res/values/styles_holo.xml | 23 ++ core/res/res/values/styles_material.xml | 41 ++++ core/res/res/values/symbols.xml | 5 + core/res/res/values/themes.xml | 2 - core/res/res/values/themes_holo.xml | 6 +- core/res/res/values/themes_material.xml | 9 +- core/tests/coretests/AndroidManifest.xml | 3 +- .../android/widget/SuggestionsPopupWindowTest.java | 25 +- 16 files changed, 372 insertions(+), 152 deletions(-) create mode 100644 core/res/res/layout/text_edit_suggestion_container.xml create mode 100644 core/res/res/layout/text_edit_suggestion_container_material.xml create mode 100644 core/res/res/layout/text_edit_suggestion_item_material.xml diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 2983053ab3ff..9a911bcd6963 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -2634,8 +2634,11 @@ public class Editor { @VisibleForTesting public class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener { private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE; - private static final int ADD_TO_DICTIONARY = -1; - private static final int DELETE_TEXT = -2; + + // Key of intent extras for inserting new word into user dictionary. + private static final String USER_DICTIONARY_EXTRA_WORD = "word"; + private static final String USER_DICTIONARY_EXTRA_LOCALE = "locale"; + private SuggestionInfo[] mSuggestionInfos; private int mNumberOfSuggestions; private boolean mCursorWasVisibleBeforeSuggestions; @@ -2644,7 +2647,10 @@ public class Editor { private final Comparator mSuggestionSpanComparator; private final HashMap mSpansLengths; private final TextAppearanceSpan mHighlightSpan = new TextAppearanceSpan( - mTextView.getContext(), android.R.style.TextAppearance_SuggestionHighlight); + mTextView.getContext(), mTextView.mTextEditSuggestionHighlightStyle); + private TextView mAddToDictionaryButton; + private TextView mDeleteButton; + private SuggestionSpan mMisspelledSpan; private class CustomPopupWindow extends PopupWindow { public CustomPopupWindow(Context context, int defStyleAttr) { @@ -2684,17 +2690,73 @@ public class Editor { @Override protected void initContentView() { - ListView listView = new ListView(mTextView.getContext()); + final LayoutInflater inflater = (LayoutInflater) mTextView.getContext(). + getSystemService(Context.LAYOUT_INFLATER_SERVICE); + final LinearLayout linearLayout = (LinearLayout) inflater.inflate( + mTextView.mTextEditSuggestionContainerLayout, null); + + final ListView suggestionListView = (ListView) linearLayout.findViewById( + com.android.internal.R.id.suggestionContainer); + mSuggestionsAdapter = new SuggestionAdapter(); - listView.setAdapter(mSuggestionsAdapter); - listView.setOnItemClickListener(this); - mContentView = listView; + suggestionListView.setAdapter(mSuggestionsAdapter); + suggestionListView.setOnItemClickListener(this); - // Inflate the suggestion items once and for all. + 2 for add to dictionary and delete - mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 2]; + // Inflate the suggestion items once and for all. + mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS]; for (int i = 0; i < mSuggestionInfos.length; i++) { mSuggestionInfos[i] = new SuggestionInfo(); } + + mContentView = linearLayout; + + mAddToDictionaryButton = (TextView) linearLayout.findViewById( + com.android.internal.R.id.addToDictionaryButton); + mAddToDictionaryButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + final Editable editable = (Editable) mTextView.getText(); + final int spanStart = editable.getSpanStart(mMisspelledSpan); + final int spanEnd = editable.getSpanEnd(mMisspelledSpan); + final String originalText = TextUtils.substring(editable, spanStart, spanEnd); + + final Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT); + intent.putExtra(USER_DICTIONARY_EXTRA_WORD, originalText); + intent.putExtra(USER_DICTIONARY_EXTRA_LOCALE, + mTextView.getTextServicesLocale().toString()); + intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); + mTextView.getContext().startActivity(intent); + // There is no way to know if the word was indeed added. Re-check. + // TODO The ExtractEditText should remove the span in the original text instead + editable.removeSpan(mMisspelledSpan); + Selection.setSelection(editable, spanEnd); + updateSpellCheckSpans(spanStart, spanEnd, false); + hideWithCleanUp(); + } + }); + + mDeleteButton = (TextView) linearLayout.findViewById( + com.android.internal.R.id.deleteButton); + mDeleteButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + final Editable editable = (Editable) mTextView.getText(); + + final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan); + int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan); + if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) { + // Do not leave two adjacent spaces after deletion, or one at beginning of + // text + if (spanUnionEnd < editable.length() && + Character.isSpaceChar(editable.charAt(spanUnionEnd)) && + (spanUnionStart == 0 || + Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) { + spanUnionEnd = spanUnionEnd + 1; + } + mTextView.deleteText_internal(spanUnionStart, spanUnionEnd); + } + hideWithCleanUp(); + } + }); + } public boolean isShowingUp() { @@ -2753,14 +2815,6 @@ public class Editor { final SuggestionInfo suggestionInfo = mSuggestionInfos[position]; textView.setText(suggestionInfo.text); - - if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY || - suggestionInfo.suggestionIndex == DELETE_TEXT) { - textView.setBackgroundColor(Color.TRANSPARENT); - } else { - textView.setBackgroundColor(Color.WHITE); - } - return textView; } } @@ -2843,6 +2897,14 @@ public class Editor { width = Math.max(width, view.getMeasuredWidth()); } + if (mAddToDictionaryButton.getVisibility() != View.GONE) { + mAddToDictionaryButton.measure(horizontalMeasure, verticalMeasure); + width = Math.max(width, mAddToDictionaryButton.getMeasuredWidth()); + } + + mDeleteButton.measure(horizontalMeasure, verticalMeasure); + width = Math.max(width, mDeleteButton.getMeasuredWidth()); + // Enforce the width based on actual text widths mContentView.measure( View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), @@ -2878,6 +2940,7 @@ public class Editor { for (final SuggestionInfo info : mSuggestionInfos) { info.clear(); } + mMisspelledSpan = null; hide(); } @@ -2893,7 +2956,7 @@ public class Editor { int spanUnionStart = mTextView.getText().length(); int spanUnionEnd = 0; - SuggestionSpan misspelledSpan = null; + mMisspelledSpan = null; int underlineColor = 0; for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) { @@ -2904,7 +2967,7 @@ public class Editor { spanUnionEnd = Math.max(spanEnd, spanUnionEnd); if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) { - misspelledSpan = suggestionSpan; + mMisspelledSpan = suggestionSpan; } // The first span dictates the background color of the highlighted text @@ -2949,31 +3012,16 @@ public class Editor { highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd); } - // Add "Add to dictionary" item if there is a span with the misspelled flag - if (misspelledSpan != null) { - final int misspelledStart = spannable.getSpanStart(misspelledSpan); - final int misspelledEnd = spannable.getSpanEnd(misspelledSpan); + // Make "Add to dictionary" item visible if there is a span with the misspelled flag + int addToDictionaryButtonVisibility = View.GONE; + if (mMisspelledSpan != null) { + final int misspelledStart = spannable.getSpanStart(mMisspelledSpan); + final int misspelledEnd = spannable.getSpanEnd(mMisspelledSpan); if (misspelledStart >= 0 && misspelledEnd > misspelledStart) { - SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions]; - suggestionInfo.suggestionSpan = misspelledSpan; - suggestionInfo.suggestionIndex = ADD_TO_DICTIONARY; - suggestionInfo.text.replace(0, suggestionInfo.text.length(), mTextView. - getContext().getString(com.android.internal.R.string.addToDictionary)); - suggestionInfo.text.setSpan(mHighlightSpan, 0, 0, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - - mNumberOfSuggestions++; + addToDictionaryButtonVisibility = View.VISIBLE; } } - - // Delete item - SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions]; - suggestionInfo.suggestionSpan = null; - suggestionInfo.suggestionIndex = DELETE_TEXT; - suggestionInfo.text.replace(0, suggestionInfo.text.length(), - mTextView.getContext().getString(com.android.internal.R.string.deleteText)); - suggestionInfo.text.setSpan(mHighlightSpan, 0, 0, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - mNumberOfSuggestions++; + mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility); if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan(); if (underlineColor == 0) { @@ -3017,23 +3065,6 @@ public class Editor { Editable editable = (Editable) mTextView.getText(); SuggestionInfo suggestionInfo = mSuggestionInfos[position]; - if (suggestionInfo.suggestionIndex == DELETE_TEXT) { - final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan); - int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan); - if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) { - // Do not leave two adjacent spaces after deletion, or one at beginning of text - if (spanUnionEnd < editable.length() && - Character.isSpaceChar(editable.charAt(spanUnionEnd)) && - (spanUnionStart == 0 || - Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) { - spanUnionEnd = spanUnionEnd + 1; - } - mTextView.deleteText_internal(spanUnionStart, spanUnionEnd); - } - hideWithCleanUp(); - return; - } - final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan); final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan); if (spanStart < 0 || spanEnd <= spanStart) { @@ -3044,75 +3075,59 @@ public class Editor { final String originalText = TextUtils.substring(editable, spanStart, spanEnd); - if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) { - Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT); - intent.putExtra("word", originalText); - intent.putExtra("locale", mTextView.getTextServicesLocale().toString()); - // Put a listener to replace the original text with a word which the user - // modified in a user dictionary dialog. - intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); - mTextView.getContext().startActivity(intent); - // There is no way to know if the word was indeed added. Re-check. - // TODO The ExtractEditText should remove the span in the original text instead - editable.removeSpan(suggestionInfo.suggestionSpan); - Selection.setSelection(editable, spanEnd); - updateSpellCheckSpans(spanStart, spanEnd, false); - } else { - // SuggestionSpans are removed by replace: save them before - SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd, - SuggestionSpan.class); - final int length = suggestionSpans.length; - int[] suggestionSpansStarts = new int[length]; - int[] suggestionSpansEnds = new int[length]; - int[] suggestionSpansFlags = new int[length]; - for (int i = 0; i < length; i++) { - final SuggestionSpan suggestionSpan = suggestionSpans[i]; - suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan); - suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan); - suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan); - - // Remove potential misspelled flags - int suggestionSpanFlags = suggestionSpan.getFlags(); - if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) { - suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED; - suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT; - suggestionSpan.setFlags(suggestionSpanFlags); - } + // SuggestionSpans are removed by replace: save them before + final SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd, + SuggestionSpan.class); + final int length = suggestionSpans.length; + final int[] suggestionSpansStarts = new int[length]; + final int[] suggestionSpansEnds = new int[length]; + final int[] suggestionSpansFlags = new int[length]; + for (int i = 0; i < length; i++) { + final SuggestionSpan suggestionSpan = suggestionSpans[i]; + suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan); + suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan); + suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan); + + // Remove potential misspelled flags + int suggestionSpanFlags = suggestionSpan.getFlags(); + if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) { + suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED; + suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT; + suggestionSpan.setFlags(suggestionSpanFlags); } + } - final int suggestionStart = suggestionInfo.suggestionStart; - final int suggestionEnd = suggestionInfo.suggestionEnd; - final String suggestion = suggestionInfo.text.subSequence( - suggestionStart, suggestionEnd).toString(); - mTextView.replaceText_internal(spanStart, spanEnd, suggestion); - - // Notify source IME of the suggestion pick. Do this before - // swaping texts. - suggestionInfo.suggestionSpan.notifySelection( - mTextView.getContext(), originalText, suggestionInfo.suggestionIndex); - - // Swap text content between actual text and Suggestion span - String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions(); - suggestions[suggestionInfo.suggestionIndex] = originalText; - - // Restore previous SuggestionSpans - final int lengthDifference = suggestion.length() - (spanEnd - spanStart); - for (int i = 0; i < length; i++) { - // Only spans that include the modified region make sense after replacement - // Spans partially included in the replaced region are removed, there is no - // way to assign them a valid range after replacement - if (suggestionSpansStarts[i] <= spanStart && - suggestionSpansEnds[i] >= spanEnd) { - mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i], - suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]); - } + final int suggestionStart = suggestionInfo.suggestionStart; + final int suggestionEnd = suggestionInfo.suggestionEnd; + final String suggestion = suggestionInfo.text.subSequence( + suggestionStart, suggestionEnd).toString(); + mTextView.replaceText_internal(spanStart, spanEnd, suggestion); + + // Notify source IME of the suggestion pick. Do this before + // swaping texts. + suggestionInfo.suggestionSpan.notifySelection( + mTextView.getContext(), originalText, suggestionInfo.suggestionIndex); + + // Swap text content between actual text and Suggestion span + final String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions(); + suggestions[suggestionInfo.suggestionIndex] = originalText; + + // Restore previous SuggestionSpans + final int lengthDifference = suggestion.length() - (spanEnd - spanStart); + for (int i = 0; i < length; i++) { + // Only spans that include the modified region make sense after replacement + // Spans partially included in the replaced region are removed, there is no + // way to assign them a valid range after replacement + if (suggestionSpansStarts[i] <= spanStart && + suggestionSpansEnds[i] >= spanEnd) { + mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i], + suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]); } - - // Move cursor at the end of the replaced word - final int newCursorPosition = spanEnd + lengthDifference; - mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition); } + // Move cursor at the end of the replaced word + final int newCursorPosition = spanEnd + lengthDifference; + mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition); hideWithCleanUp(); } } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index eaf4fe2c71b2..476c6a268c82 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -624,7 +624,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // Although these fields are specific to editable text, they are not added to Editor because // they are defined by the TextView's style and are theme-dependent. int mCursorDrawableRes; - // These four fields, could be moved to Editor, since we know their default values and we + // These six fields, could be moved to Editor, since we know their default values and we // could condition the creation of the Editor to a non standard value. This is however // brittle since the hardcoded values here (such as // com.android.internal.R.drawable.text_select_handle_left) would have to be updated if the @@ -633,6 +633,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int mTextSelectHandleRightRes; int mTextSelectHandleRes; int mTextEditSuggestionItemLayout; + int mTextEditSuggestionContainerLayout; + int mTextEditSuggestionHighlightStyle; /** * EditText specific data, created on demand when one of the Editor fields is used. @@ -1155,6 +1157,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mTextEditSuggestionItemLayout = a.getResourceId(attr, 0); break; + case com.android.internal.R.styleable.TextView_textEditSuggestionContainerLayout: + mTextEditSuggestionContainerLayout = a.getResourceId(attr, 0); + break; + + case com.android.internal.R.styleable.TextView_textEditSuggestionHighlightStyle: + mTextEditSuggestionHighlightStyle = a.getResourceId(attr, 0); + break; + case com.android.internal.R.styleable.TextView_textIsSelectable: setTextIsSelectable(a.getBoolean(attr, false)); break; diff --git a/core/res/res/layout/text_edit_suggestion_container.xml b/core/res/res/layout/text_edit_suggestion_container.xml new file mode 100644 index 000000000000..04eca8f721e7 --- /dev/null +++ b/core/res/res/layout/text_edit_suggestion_container.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + diff --git a/core/res/res/layout/text_edit_suggestion_container_material.xml b/core/res/res/layout/text_edit_suggestion_container_material.xml new file mode 100644 index 000000000000..d0e24670b025 --- /dev/null +++ b/core/res/res/layout/text_edit_suggestion_container_material.xml @@ -0,0 +1,42 @@ + + + + + + + + + + diff --git a/core/res/res/layout/text_edit_suggestion_item.xml b/core/res/res/layout/text_edit_suggestion_item.xml index a965ddd0f9ca..9dcbf2e1ff39 100644 --- a/core/res/res/layout/text_edit_suggestion_item.xml +++ b/core/res/res/layout/text_edit_suggestion_item.xml @@ -4,9 +4,9 @@ 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. @@ -15,16 +15,5 @@ --> + style="@android:style/Widget.Holo.SuggestionItem" /> diff --git a/core/res/res/layout/text_edit_suggestion_item_material.xml b/core/res/res/layout/text_edit_suggestion_item_material.xml new file mode 100644 index 000000000000..0443a976dc11 --- /dev/null +++ b/core/res/res/layout/text_edit_suggestion_item_material.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 04f4fc2d4652..f9f8162c1051 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -942,6 +942,10 @@ i + + + + @@ -4411,6 +4415,10 @@ i + + + + diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml index eb99077beff6..e6f279d4d53c 100644 --- a/core/res/res/values/styles.xml +++ b/core/res/res/values/styles.xml @@ -496,6 +496,8 @@ please see styles_device_defaults.xml. ?attr/textEditSidePasteWindowLayout ?attr/textEditSideNoPasteWindowLayout ?attr/textEditSuggestionItemLayout + ?attr/textEditSuggestionContainerLayout + ?attr/textEditSuggestionHighlightStyle ?attr/textCursorDrawable high_quality normal @@ -954,10 +956,10 @@ please see styles_device_defaults.xml. 6dip - + diff --git a/core/res/res/values/styles_holo.xml b/core/res/res/values/styles_holo.xml index 686106951b63..841afd8a35d4 100644 --- a/core/res/res/values/styles_holo.xml +++ b/core/res/res/values/styles_holo.xml @@ -1176,4 +1176,27 @@ please see styles_device_defaults.xml. + + diff --git a/core/res/res/values/styles_material.xml b/core/res/res/values/styles_material.xml index 4b2a4518b36c..adcb79b9ff7f 100644 --- a/core/res/res/values/styles_material.xml +++ b/core/res/res/values/styles_material.xml @@ -988,6 +988,47 @@ please see styles_device_defaults.xml. @string/media_route_button_content_description + + + + + +