diff --git a/api/current.xml b/api/current.xml index d79fab6645e49..3c2fccb5eb828 100644 --- a/api/current.xml +++ b/api/current.xml @@ -16883,6 +16883,17 @@ visibility="public" > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + = 0) { - boolean consumed = mDropDownList.onKeyUp(keyCode, event); - if (consumed) { - switch (keyCode) { - // if the list accepts the key events and the key event - // was a click, the text view gets the selected item - // from the drop down as its content - case KeyEvent.KEYCODE_ENTER: - case KeyEvent.KEYCODE_DPAD_CENTER: - performCompletion(); - return true; - } + boolean consumed = mPopup.onKeyUp(keyCode, event); + if (consumed) { + switch (keyCode) { + // if the list accepts the key events and the key event + // was a click, the text view gets the selected item + // from the drop down as its content + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + performCompletion(); + return true; } } return super.onKeyUp(keyCode, event); @@ -652,87 +636,11 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - // when the drop down is shown, we drive it directly - if (isPopupShowing()) { - // the key events are forwarded to the list in the drop down view - // note that ListView handles space but we don't want that to happen - // also if selection is not currently in the drop down, then don't - // let center or enter presses go there since that would cause it - // to select one of its items - if (keyCode != KeyEvent.KEYCODE_SPACE - && (mDropDownList.getSelectedItemPosition() >= 0 - || (keyCode != KeyEvent.KEYCODE_ENTER - && keyCode != KeyEvent.KEYCODE_DPAD_CENTER))) { - int curIndex = mDropDownList.getSelectedItemPosition(); - boolean consumed; - - final boolean below = !mPopup.isAboveAnchor(); - - final ListAdapter adapter = mAdapter; - - boolean allEnabled; - int firstItem = Integer.MAX_VALUE; - int lastItem = Integer.MIN_VALUE; - - if (adapter != null) { - allEnabled = adapter.areAllItemsEnabled(); - firstItem = allEnabled ? 0 : - mDropDownList.lookForSelectablePosition(0, true); - lastItem = allEnabled ? adapter.getCount() - 1 : - mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false); - } - - if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) || - (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) { - // When the selection is at the top, we block the key - // event to prevent focus from moving. - clearListSelection(); - mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); - showDropDown(); - return true; - } else { - // WARNING: Please read the comment where mListSelectionHidden - // is declared - mDropDownList.mListSelectionHidden = false; - } - - consumed = mDropDownList.onKeyDown(keyCode, event); - if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed); - - if (consumed) { - // If it handled the key event, then the user is - // navigating in the list, so we should put it in front. - mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); - // Here's a little trick we need to do to make sure that - // the list view is actually showing its focus indicator, - // by ensuring it has focus and getting its window out - // of touch mode. - mDropDownList.requestFocusFromTouch(); - showDropDown(); - - switch (keyCode) { - // avoid passing the focus from the text view to the - // next component - case KeyEvent.KEYCODE_ENTER: - case KeyEvent.KEYCODE_DPAD_CENTER: - case KeyEvent.KEYCODE_DPAD_DOWN: - case KeyEvent.KEYCODE_DPAD_UP: - return true; - } - } else { - if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { - // when the selection is at the bottom, we block the - // event to avoid going to the next focusable widget - if (curIndex == lastItem) { - return true; - } - } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP && - curIndex == firstItem) { - return true; - } - } - } - } else { + if (mPopup.onKeyDown(keyCode, event)) { + return true; + } + + if (!isPopupShowing()) { switch(keyCode) { case KeyEvent.KEYCODE_DPAD_DOWN: performValidation(); @@ -743,7 +651,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe boolean handled = super.onKeyDown(keyCode, event); mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; - if (handled && isPopupShowing() && mDropDownList != null) { + if (handled && isPopupShowing()) { clearListSelection(); } @@ -804,11 +712,12 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe if (enoughToFilter()) { if (mFilter != null) { performFiltering(getText(), mLastKeyCode); + buildImeCompletions(); } } else { // drop down is automatically dismissed when enough characters // are deleted from the text view - if (!mDropDownAlwaysVisible) dismissDropDown(); + if (!mPopup.isDropDownAlwaysVisible()) dismissDropDown(); if (mFilter != null) { mFilter.filter(null); } @@ -841,13 +750,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * it back. */ public void clearListSelection() { - final DropDownListView list = mDropDownList; - if (list != null) { - // WARNING: Please read the comment where mListSelectionHidden is declared - list.mListSelectionHidden = true; - list.hideSelector(); - list.requestLayout(); - } + mPopup.clearListSelection(); } /** @@ -856,11 +759,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @param position The position to move the selector to. */ public void setListSelection(int position) { - if (mPopup.isShowing() && (mDropDownList != null)) { - mDropDownList.mListSelectionHidden = false; - mDropDownList.setSelection(position); - // ListView.setSelection() will call requestLayout() - } + mPopup.setSelection(position); } /** @@ -874,10 +773,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @see ListView#getSelectedItemPosition() */ public int getListSelection() { - if (mPopup.isShowing() && (mDropDownList != null)) { - return mDropDownList.getSelectedItemPosition(); - } - return ListView.INVALID_POSITION; + return mPopup.getSelectedItemPosition(); } /** @@ -911,13 +807,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe replaceText(completion.getText()); mBlockCompletion = false; - if (mItemClickListener != null) { - final DropDownListView list = mDropDownList; - final int position = completion.getPosition(); - mItemClickListener.onItemClick(list, - list.getChildAt(position - list.getFirstVisiblePosition()), - position, completion.getId()); - } + mPopup.performItemClick(completion.getPosition()); } } @@ -925,7 +815,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe if (isPopupShowing()) { Object selectedItem; if (position < 0) { - selectedItem = mDropDownList.getSelectedItem(); + selectedItem = mPopup.getSelectedItem(); } else { selectedItem = mAdapter.getItem(position); } @@ -939,18 +829,18 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe mBlockCompletion = false; if (mItemClickListener != null) { - final DropDownListView list = mDropDownList; + final ListPopupWindow list = mPopup; if (selectedView == null || position < 0) { selectedView = list.getSelectedView(); position = list.getSelectedItemPosition(); id = list.getSelectedItemId(); } - mItemClickListener.onItemClick(list, selectedView, position, id); + mItemClickListener.onItemClick(list.getListView(), selectedView, position, id); } } - if (mDropDownDismissedOnCompletion && !mDropDownAlwaysVisible) { + if (mDropDownDismissedOnCompletion && !mPopup.isDropDownAlwaysVisible()) { dismissDropDown(); } } @@ -1000,7 +890,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe /** {@inheritDoc} */ public void onFilterComplete(int count) { updateDropDownForFilter(count); - } private void updateDropDownForFilter(int count) { @@ -1014,11 +903,12 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * to filter. */ - if ((count > 0 || mDropDownAlwaysVisible) && enoughToFilter()) { + final boolean dropDownAlwaysVisible = mPopup.isDropDownAlwaysVisible(); + if ((count > 0 || dropDownAlwaysVisible) && enoughToFilter()) { if (hasFocus() && hasWindowFocus()) { showDropDown(); } - } else if (!mDropDownAlwaysVisible) { + } else if (!dropDownAlwaysVisible) { dismissDropDown(); } } @@ -1026,7 +916,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); - if (!hasWindowFocus && !mDropDownAlwaysVisible) { + if (!hasWindowFocus && !mPopup.isDropDownAlwaysVisible()) { dismissDropDown(); } } @@ -1036,7 +926,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe super.onDisplayHint(hint); switch (hint) { case INVISIBLE: - if (!mDropDownAlwaysVisible) { + if (!mPopup.isDropDownAlwaysVisible()) { dismissDropDown(); } break; @@ -1050,7 +940,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe if (!focused) { performValidation(); } - if (!focused && !mDropDownAlwaysVisible) { + if (!focused && !mPopup.isDropDownAlwaysVisible()) { dismissDropDown(); } } @@ -1075,8 +965,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe imm.displayCompletions(this, null); } mPopup.dismiss(); - mPopup.setContentView(null); - mDropDownList = null; } @Override @@ -1089,18 +977,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe return result; } - - /** - *

Used for lazy instantiation of the anchor view from the id we have. If the value of - * the id is NO_ID or we can't find a view for the given id, we return this TextView as - * the default anchoring point.

- */ - private View getDropDownAnchorView() { - if (mDropDownAnchorView == null && mDropDownAnchorId != View.NO_ID) { - mDropDownAnchorView = getRootView().findViewById(mDropDownAnchorId); - } - return mDropDownAnchorView == null ? this : mDropDownAnchorView; - } /** * Issues a runnable to show the dropdown as soon as possible. @@ -1108,7 +984,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @hide internal used only by SearchDialog */ public void showDropDownAfterLayout() { - post(mShowDropDownRunnable); + mPopup.postShow(); } /** @@ -1119,7 +995,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe */ public void ensureImeVisible(boolean visible) { mPopup.setInputMethodMode(visible - ? PopupWindow.INPUT_METHOD_NEEDED : PopupWindow.INPUT_METHOD_NOT_NEEDED); + ? ListPopupWindow.INPUT_METHOD_NEEDED : ListPopupWindow.INPUT_METHOD_NOT_NEEDED); showDropDown(); } @@ -1127,89 +1003,21 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @hide internal used only here and SearchDialog */ public boolean isInputMethodNotNeeded() { - return mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; + return mPopup.getInputMethodMode() == ListPopupWindow.INPUT_METHOD_NOT_NEEDED; } /** *

Displays the drop down on screen.

*/ public void showDropDown() { - int height = buildDropDown(); - - int widthSpec = 0; - int heightSpec = 0; - - boolean noInputMethod = isInputMethodNotNeeded(); - - if (mPopup.isShowing()) { - if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { - // The call to PopupWindow's update method below can accept -1 for any - // value you do not want to update. - widthSpec = -1; - } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { - widthSpec = getDropDownAnchorView().getWidth(); + if (mPopup.getAnchorView() == null) { + if (mDropDownAnchorId != View.NO_ID) { + mPopup.setAnchorView(getRootView().findViewById(mDropDownAnchorId)); } else { - widthSpec = mDropDownWidth; + mPopup.setAnchorView(this); } - - if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { - // The call to PopupWindow's update method below can accept -1 for any - // value you do not want to update. - heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT; - if (noInputMethod) { - mPopup.setWindowLayoutMode( - mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? - ViewGroup.LayoutParams.MATCH_PARENT : 0, 0); - } else { - mPopup.setWindowLayoutMode( - mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? - ViewGroup.LayoutParams.MATCH_PARENT : 0, - ViewGroup.LayoutParams.MATCH_PARENT); - } - } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { - heightSpec = height; - } else { - heightSpec = mDropDownHeight; - } - - mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); - - mPopup.update(getDropDownAnchorView(), mDropDownHorizontalOffset, - mDropDownVerticalOffset, widthSpec, heightSpec); - } else { - if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { - widthSpec = ViewGroup.LayoutParams.MATCH_PARENT; - } else { - if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { - mPopup.setWidth(getDropDownAnchorView().getWidth()); - } else { - mPopup.setWidth(mDropDownWidth); - } - } - - if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { - heightSpec = ViewGroup.LayoutParams.MATCH_PARENT; - } else { - if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { - mPopup.setHeight(height); - } else { - mPopup.setHeight(mDropDownHeight); - } - } - - mPopup.setWindowLayoutMode(widthSpec, heightSpec); - mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); - - // use outside touchable to dismiss drop down when touching outside of it, so - // only set this if the dropdown is not always visible - mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); - mPopup.setTouchInterceptor(new PopupTouchInterceptor()); - mPopup.showAsDropDown(getDropDownAnchorView(), - mDropDownHorizontalOffset, mDropDownVerticalOffset); - mDropDownList.setSelection(ListView.INVALID_POSITION); - clearListSelection(); - post(mHideSelector); } + mPopup.show(); } /** @@ -1220,19 +1028,10 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @hide used only by SearchDialog */ public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) { - mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch; + mPopup.setForceIgnoreOutsideTouch(forceIgnoreOutsideTouch); } - - /** - *

Builds the popup window's content and returns the height the popup - * should have. Returns -1 when the content already exists.

- * - * @return the content's height or -1 if content already exists - */ - private int buildDropDown() { - ViewGroup dropDownView; - int otherHeights = 0; - + + private void buildImeCompletions() { final ListAdapter adapter = mAdapter; if (adapter != null) { InputMethodManager imm = InputMethodManager.peekInstance(); @@ -1260,135 +1059,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe imm.displayCompletions(this, completions); } } - - if (mDropDownList == null) { - Context context = getContext(); - - mHideSelector = new ListSelectorHider(); - - /** - * This Runnable exists for the sole purpose of checking if the view layout has got - * completed and if so call showDropDown to display the drop down. This is used to show - * the drop down as soon as possible after user opens up the search dialog, without - * waiting for the normal UI pipeline to do it's job which is slower than this method. - */ - mShowDropDownRunnable = new Runnable() { - public void run() { - // View layout should be all done before displaying the drop down. - View view = getDropDownAnchorView(); - if (view != null && view.getWindowToken() != null) { - showDropDown(); - } - } - }; - - mDropDownList = new DropDownListView(context); - mDropDownList.setSelector(mDropDownListHighlight); - mDropDownList.setAdapter(adapter); - mDropDownList.setVerticalFadingEdgeEnabled(true); - mDropDownList.setOnItemClickListener(mDropDownItemClickListener); - mDropDownList.setFocusable(true); - mDropDownList.setFocusableInTouchMode(true); - mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - public void onItemSelected(AdapterView parent, View view, - int position, long id) { - - if (position != -1) { - DropDownListView dropDownList = mDropDownList; - - if (dropDownList != null) { - dropDownList.mListSelectionHidden = false; - } - } - } - - public void onNothingSelected(AdapterView parent) { - } - }); - mDropDownList.setOnScrollListener(new PopupScrollListener()); - - if (mItemSelectedListener != null) { - mDropDownList.setOnItemSelectedListener(mItemSelectedListener); - } - - dropDownView = mDropDownList; - - View hintView = getHintView(context); - if (hintView != null) { - // if an hint has been specified, we accomodate more space for it and - // add a text view in the drop down menu, at the bottom of the list - LinearLayout hintContainer = new LinearLayout(context); - hintContainer.setOrientation(LinearLayout.VERTICAL); - - LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f - ); - hintContainer.addView(dropDownView, hintParams); - hintContainer.addView(hintView); - - // measure the hint's height to find how much more vertical space - // we need to add to the drop down's height - int widthSpec = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST); - int heightSpec = MeasureSpec.UNSPECIFIED; - hintView.measure(widthSpec, heightSpec); - - hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams(); - otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin - + hintParams.bottomMargin; - - dropDownView = hintContainer; - } - - mPopup.setContentView(dropDownView); - } else { - dropDownView = (ViewGroup) mPopup.getContentView(); - final View view = dropDownView.findViewById(HINT_VIEW_ID); - if (view != null) { - LinearLayout.LayoutParams hintParams = - (LinearLayout.LayoutParams) view.getLayoutParams(); - otherHeights = view.getMeasuredHeight() + hintParams.topMargin - + hintParams.bottomMargin; - } - } - - // Max height available on the screen for a popup. - boolean ignoreBottomDecorations = - mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; - final int maxHeight = mPopup.getMaxAvailableHeight( - getDropDownAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations); - - // getMaxAvailableHeight() subtracts the padding, so we put it back, - // to get the available height for the whole window - int padding = 0; - Drawable background = mPopup.getBackground(); - if (background != null) { - background.getPadding(mTempRect); - padding = mTempRect.top + mTempRect.bottom; - } - - if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { - return maxHeight + padding; - } - - final int listContent = mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED, - 0, ListView.NO_POSITION, maxHeight - otherHeights, 2); - // add padding only if the list has items in it, that way we don't show - // the popup if it is not needed - if (listContent > 0) otherHeights += padding; - - return listContent + otherHeights; - } - - private View getHintView(Context context) { - if (mHintText != null && mHintText.length() > 0) { - final TextView hintView = (TextView) LayoutInflater.from(context).inflate( - mHintResource, null).findViewById(com.android.internal.R.id.text1); - hintView.setText(mHintText); - hintView.setId(HINT_VIEW_ID); - return hintView; - } else { - return null; - } } /** @@ -1440,170 +1110,12 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe return mFilter; } - private class ListSelectorHider implements Runnable { - public void run() { - clearListSelection(); - } - } - - private class ResizePopupRunnable implements Runnable { - public void run() { - mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); - showDropDown(); - } - } - - private class PopupTouchInterceptor implements OnTouchListener { - public boolean onTouch(View v, MotionEvent event) { - final int action = event.getAction(); - if (action == MotionEvent.ACTION_DOWN && - mPopup != null && mPopup.isShowing()) { - postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT); - } else if (action == MotionEvent.ACTION_UP) { - removeCallbacks(mResizePopupRunnable); - } - return false; - } - } - - private class PopupScrollListener implements ListView.OnScrollListener { - public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, - int totalItemCount) { - - } - - public void onScrollStateChanged(AbsListView view, int scrollState) { - if (scrollState == SCROLL_STATE_TOUCH_SCROLL && - !isInputMethodNotNeeded() && mPopup.getContentView() != null) { - removeCallbacks(mResizePopupRunnable); - mResizePopupRunnable.run(); - } - } - } - private class DropDownItemClickListener implements AdapterView.OnItemClickListener { public void onItemClick(AdapterView parent, View v, int position, long id) { performCompletion(v, position, id); } } - /** - *

Wrapper class for a ListView. This wrapper hijacks the focus to - * make sure the list uses the appropriate drawables and states when - * displayed on screen within a drop down. The focus is never actually - * passed to the drop down; the list only looks focused.

- */ - private static class DropDownListView extends ListView { - /* - * WARNING: This is a workaround for a touch mode issue. - * - * Touch mode is propagated lazily to windows. This causes problems in - * the following scenario: - * - Type something in the AutoCompleteTextView and get some results - * - Move down with the d-pad to select an item in the list - * - Move up with the d-pad until the selection disappears - * - Type more text in the AutoCompleteTextView *using the soft keyboard* - * and get new results; you are now in touch mode - * - The selection comes back on the first item in the list, even though - * the list is supposed to be in touch mode - * - * Using the soft keyboard triggers the touch mode change but that change - * is propagated to our window only after the first list layout, therefore - * after the list attempts to resurrect the selection. - * - * The trick to work around this issue is to pretend the list is in touch - * mode when we know that the selection should not appear, that is when - * we know the user moved the selection away from the list. - * - * This boolean is set to true whenever we explicitely hide the list's - * selection and reset to false whenver we know the user moved the - * selection back to the list. - * - * When this boolean is true, isInTouchMode() returns true, otherwise it - * returns super.isInTouchMode(). - */ - private boolean mListSelectionHidden; - - /** - *

Creates a new list view wrapper.

- * - * @param context this view's context - */ - public DropDownListView(Context context) { - super(context, null, com.android.internal.R.attr.dropDownListViewStyle); - } - - /** - *

Avoids jarring scrolling effect by ensuring that list elements - * made of a text view fit on a single line.

- * - * @param position the item index in the list to get a view for - * @return the view for the specified item - */ - @Override - View obtainView(int position, boolean[] isScrap) { - View view = super.obtainView(position, isScrap); - - if (view instanceof TextView) { - ((TextView) view).setHorizontallyScrolling(true); - } - - return view; - } - - @Override - public boolean isInTouchMode() { - // WARNING: Please read the comment where mListSelectionHidden is declared - return mListSelectionHidden || super.isInTouchMode(); - } - - /** - *

Returns the focus state in the drop down.

- * - * @return true always - */ - @Override - public boolean hasWindowFocus() { - return true; - } - - /** - *

Returns the focus state in the drop down.

- * - * @return true always - */ - @Override - public boolean isFocused() { - return true; - } - - /** - *

Returns the focus state in the drop down.

- * - * @return true always - */ - @Override - public boolean hasFocus() { - return true; - } - - protected int[] onCreateDrawableState(int extraSpace) { - int[] res = super.onCreateDrawableState(extraSpace); - //noinspection ConstantIfStatement - if (false) { - StringBuilder sb = new StringBuilder("Created drawable state: ["); - for (int i=0; i 0) sb.append(", "); - sb.append("0x"); - sb.append(Integer.toHexString(res[i])); - } - sb.append("]"); - Log.i(TAG, sb.toString()); - } - return res; - } - } - /** * This interface is used to make sure that the text entered in this TextView complies to * a certain format. Since there is no foolproof way to prevent the user from leaving @@ -1652,10 +1164,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe private class PopupDataSetObserver extends DataSetObserver { @Override public void onChanged() { - if (isPopupShowing()) { - // This will resize the popup to fit the new adapter's content - showDropDown(); - } else if (mAdapter != null) { + if (mAdapter != null) { // If the popup is not showing already, showing it will cause // the list of data set observers attached to the adapter to // change. We can't do it from here, because we are in the middle @@ -1670,14 +1179,5 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe }); } } - - @Override - public void onInvalidated() { - if (!mDropDownAlwaysVisible) { - // There's no data to display so make sure we're not showing - // the drop down and its list - dismissDropDown(); - } - } } } diff --git a/core/java/android/widget/ListPopupWindow.java b/core/java/android/widget/ListPopupWindow.java new file mode 100644 index 0000000000000..eda36064986fc --- /dev/null +++ b/core/java/android/widget/ListPopupWindow.java @@ -0,0 +1,1215 @@ +/* + * 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 android.widget; + +import android.content.Context; +import android.database.DataSetObserver; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.View.MeasureSpec; +import android.view.View.OnTouchListener; + +/** + * A ListPopupWindow anchors itself to a host view and displays a + * list of choices. When one is selected, the popup is dismissed. + * + *

ListPopupWindow contains a number of tricky behaviors surrounding + * positioning, scrolling parents to fit the dropdown, interacting + * sanely with the IME if present, and others. + * + * @see android.widget.AutoCompleteTextView + * @see android.widget.Spinner + */ +public class ListPopupWindow { + private static final String TAG = "ListPopupWindow"; + private static final boolean DEBUG = false; + + /** + * This value controls the length of time that the user + * must leave a pointer down without scrolling to expand + * the autocomplete dropdown list to cover the IME. + */ + private static final int EXPAND_LIST_TIMEOUT = 250; + + private Context mContext; + private PopupWindow mPopup; + private ListAdapter mAdapter; + private DropDownListView mDropDownList; + + private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT; + private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT; + private int mDropDownHorizontalOffset; + private int mDropDownVerticalOffset; + + private boolean mDropDownAlwaysVisible = false; + private boolean mForceIgnoreOutsideTouch = false; + + private View mPromptView; + private int mPromptPosition = POSITION_PROMPT_ABOVE; + + private DataSetObserver mObserver; + + private View mDropDownAnchorView; + + private Drawable mDropDownListHighlight; + + private AdapterView.OnItemClickListener mItemClickListener; + private AdapterView.OnItemSelectedListener mItemSelectedListener; + + private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable(); + private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor(); + private final PopupScrollListener mScrollListener = new PopupScrollListener(); + private final ListSelectorHider mHideSelector = new ListSelectorHider(); + private Runnable mShowDropDownRunnable; + + private Handler mHandler = new Handler(); + + private Rect mTempRect = new Rect(); + + private boolean mModal; + + /** + * The provided prompt view should appear above list content. + * + * @see #setPromptPosition(int) + * @see #getPromptPosition() + * @see #setPromptView(View) + */ + public static final int POSITION_PROMPT_ABOVE = 0; + + /** + * The provided prompt view should appear below list content. + * + * @see #setPromptPosition(int) + * @see #getPromptPosition() + * @see #setPromptView(View) + */ + public static final int POSITION_PROMPT_BELOW = 1; + + /** + * Alias for {@link ViewGroup.LayoutParams#MATCH_PARENT}. + * If used to specify a popup width, the popup will match the width of the anchor view. + * If used to specify a popup height, the popup will fill available space. + */ + public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT; + + /** + * Alias for {@link ViewGroup.LayoutParams#WRAP_CONTENT}. + * If used to specify a popup width, the popup will use the width of its content. + */ + public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT; + + /** + * Mode for {@link #setInputMethodMode(int)}: the requirements for the + * input method should be based on the focusability of the popup. That is + * if it is focusable than it needs to work with the input method, else + * it doesn't. + */ + public static final int INPUT_METHOD_FROM_FOCUSABLE = PopupWindow.INPUT_METHOD_FROM_FOCUSABLE; + + /** + * Mode for {@link #setInputMethodMode(int)}: this popup always needs to + * work with an input method, regardless of whether it is focusable. This + * means that it will always be displayed so that the user can also operate + * the input method while it is shown. + */ + public static final int INPUT_METHOD_NEEDED = PopupWindow.INPUT_METHOD_NEEDED; + + /** + * Mode for {@link #setInputMethodMode(int)}: this popup never needs to + * work with an input method, regardless of whether it is focusable. This + * means that it will always be displayed to use as much space on the + * screen as needed, regardless of whether this covers the input method. + */ + public static final int INPUT_METHOD_NOT_NEEDED = PopupWindow.INPUT_METHOD_NOT_NEEDED; + + /** + * Create a new, empty popup window capable of displaying items from a ListAdapter. + * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. + * + * @param context Context used for contained views. + */ + public ListPopupWindow(Context context) { + this(context, null, 0, 0); + } + + /** + * Create a new, empty popup window capable of displaying items from a ListAdapter. + * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. + * + * @param context Context used for contained views. + * @param attrs Attributes from inflating parent views used to style the popup. + */ + public ListPopupWindow(Context context, AttributeSet attrs) { + this(context, attrs, 0, 0); + } + + /** + * Create a new, empty popup window capable of displaying items from a ListAdapter. + * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. + * + * @param context Context used for contained views. + * @param attrs Attributes from inflating parent views used to style the popup. + * @param defStyleAttr Default style attribute to use for popup content. + */ + public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + /** + * Create a new, empty popup window capable of displaying items from a ListAdapter. + * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}. + * + * @param context Context used for contained views. + * @param attrs Attributes from inflating parent views used to style the popup. + * @param defStyleAttr Style attribute to read for default styling of popup content. + * @param defStyleRes Style resource ID to use for default styling of popup content. + */ + public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + mContext = context; + mPopup = new PopupWindow(context, attrs, defStyleAttr, defStyleRes); + } + + /** + * Sets the adapter that provides the data and the views to represent the data + * in this popup window. + * + * @param adapter The adapter to use to create this window's content. + */ + public void setAdapter(ListAdapter adapter) { + if (mObserver == null) { + mObserver = new PopupDataSetObserver(); + } else if (mAdapter != null) { + adapter.unregisterDataSetObserver(mObserver); + } + mAdapter = adapter; + if (mAdapter != null) { + adapter.registerDataSetObserver(mObserver); + } + + if (mDropDownList != null) { + mDropDownList.setAdapter(mAdapter); + } + } + + /** + * Set where the optional prompt view should appear. The default is + * {@link #POSITION_PROMPT_ABOVE}. + * + * @param position A position constant declaring where the prompt should be displayed. + * + * @see #POSITION_PROMPT_ABOVE + * @see #POSITION_PROMPT_BELOW + */ + public void setPromptPosition(int position) { + mPromptPosition = position; + } + + /** + * @return Where the optional prompt view should appear. + * + * @see #POSITION_PROMPT_ABOVE + * @see #POSITION_PROMPT_BELOW + */ + public int getPromptPosition() { + return mPromptPosition; + } + + /** + * Set whether this window should be modal when shown. + * + *

If a popup window is modal, it will receive all touch and key input. + * If the user touches outside the popup window's content area the popup window + * will be dismissed. + * + * @param modal {@code true} if the popup window should be modal, {@code false} otherwise. + */ + public void setModal(boolean modal) { + mModal = true; + mPopup.setFocusable(modal); + } + + /** + * Returns whether the popup window will be modal when shown. + * + * @return {@code true} if the popup window will be modal, {@code false} otherwise. + */ + public boolean isModal() { + return mModal; + } + + /** + * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is + * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we + * ignore outside touch even when the drop down is not set to always visible. + * + * @hide Used only by AutoCompleteTextView to handle some internal special cases. + */ + public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) { + mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch; + } + + /** + * Sets whether the drop-down should remain visible under certain conditions. + * + * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless + * of the size or content of the list. {@link #getBackground()} will fill any space + * that is not used by the list. + * + * @param dropDownAlwaysVisible Whether to keep the drop-down visible. + * + * @hide Only used by AutoCompleteTextView under special conditions. + */ + public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) { + mDropDownAlwaysVisible = dropDownAlwaysVisible; + } + + /** + * @return Whether the drop-down is visible under special conditions. + * + * @hide Only used by AutoCompleteTextView under special conditions. + */ + public boolean isDropDownAlwaysVisible() { + return mDropDownAlwaysVisible; + } + + /** + * Sets the operating mode for the soft input area. + * + * @param mode The desired mode, see + * {@link android.view.WindowManager.LayoutParams#softInputMode} + * for the full list + * + * @see android.view.WindowManager.LayoutParams#softInputMode + * @see #getSoftInputMode() + */ + public void setSoftInputMode(int mode) { + mPopup.setSoftInputMode(mode); + } + + /** + * Returns the current value in {@link #setSoftInputMode(int)}. + * + * @see #setSoftInputMode(int) + * @see android.view.WindowManager.LayoutParams#softInputMode + */ + public int getSoftInputMode() { + return mPopup.getSoftInputMode(); + } + + /** + * Sets a drawable to use as the list item selector. + * + * @param selector List selector drawable to use in the popup. + */ + public void setListSelector(Drawable selector) { + mDropDownListHighlight = selector; + } + + /** + * @return The background drawable for the popup window. + */ + public Drawable getBackground() { + return mPopup.getBackground(); + } + + /** + * Sets a drawable to be the background for the popup window. + * + * @param d A drawable to set as the background. + */ + public void setBackgroundDrawable(Drawable d) { + mPopup.setBackgroundDrawable(d); + } + + /** + * Set an animation style to use when the popup window is shown or dismissed. + * + * @param animationStyle Animation style to use. + */ + public void setAnimationStyle(int animationStyle) { + mPopup.setAnimationStyle(animationStyle); + } + + /** + * Returns the animation style that will be used when the popup window is + * shown or dismissed. + * + * @return Animation style that will be used. + */ + public int getAnimationStyle() { + return mPopup.getAnimationStyle(); + } + + /** + * Returns the view that will be used to anchor this popup. + * + * @return The popup's anchor view + */ + public View getAnchorView() { + return mDropDownAnchorView; + } + + /** + * Sets the popup's anchor view. This popup will always be positioned relative to + * the anchor view when shown. + * + * @param anchor The view to use as an anchor. + */ + public void setAnchorView(View anchor) { + mDropDownAnchorView = anchor; + } + + /** + * @return The horizontal offset of the popup from its anchor in pixels. + */ + public int getHorizontalOffset() { + return mDropDownHorizontalOffset; + } + + /** + * Set the horizontal offset of this popup from its anchor view in pixels. + * + * @param The horizontal offset of the popup from its anchor. + */ + public void setHorizontalOffset(int offset) { + mDropDownHorizontalOffset = offset; + } + + /** + * @return The vertical offset of the popup from its anchor in pixels. + */ + public int getVerticalOffset() { + return mDropDownVerticalOffset; + } + + /** + * Set the vertical offset of this popup from its anchor view in pixels. + * + * @param The vertical offset of the popup from its anchor. + */ + public void setVerticalOffset(int offset) { + mDropDownVerticalOffset = offset; + } + + /** + * @return The width of the popup window in pixels. + */ + public int getWidth() { + return mDropDownWidth; + } + + /** + * Sets the width of the popup window in pixels. Can also be {@link #MATCH_PARENT} + * or {@link #WRAP_CONTENT}. + * + * @param width Width of the popup window. + */ + public void setWidth(int width) { + mDropDownWidth = width; + } + + /** + * @return The height of the popup window in pixels. + */ + public int getHeight() { + return mDropDownHeight; + } + + /** + * Sets the height of the popup window in pixels. Can also be {@link #MATCH_PARENT}. + * + * @param height Height of the popup window. + */ + public void setHeight(int height) { + mDropDownHeight = height; + } + + /** + * Sets a listener to receive events when a list item is clicked. + * + * @param clickListener Listener to register + * + * @see ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener) + */ + public void setOnItemClickListener(AdapterView.OnItemClickListener clickListener) { + mItemClickListener = clickListener; + } + + /** + * Sets a listener to receive events when a list item is selected. + * + * @param selectedListener Listener to register. + * + * @see ListView#setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener) + */ + public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener selectedListener) { + mItemSelectedListener = selectedListener; + } + + /** + * Set a view to act as a user prompt for this popup window. Where the prompt view will appear + * is controlled by {@link #setPromptPosition(int)}. + * + * @param prompt View to use as an informational prompt. + */ + public void setPromptView(View prompt) { + boolean showing = isShowing(); + if (showing) { + removePromptView(); + } + mPromptView = prompt; + if (showing) { + show(); + } + } + + /** + * Post a {@link #show()} call to the UI thread. + */ + public void postShow() { + mHandler.post(mShowDropDownRunnable); + } + + /** + * Show the popup list. If the list is already showing, this method + * will recalculate the popup's size and position. + */ + public void show() { + int height = buildDropDown(); + + int widthSpec = 0; + int heightSpec = 0; + + boolean noInputMethod = isInputMethodNotNeeded(); + + if (mPopup.isShowing()) { + if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { + // The call to PopupWindow's update method below can accept -1 for any + // value you do not want to update. + widthSpec = -1; + } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { + widthSpec = getAnchorView().getWidth(); + } else { + widthSpec = mDropDownWidth; + } + + if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + // The call to PopupWindow's update method below can accept -1 for any + // value you do not want to update. + heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT; + if (noInputMethod) { + mPopup.setWindowLayoutMode( + mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? + ViewGroup.LayoutParams.MATCH_PARENT : 0, 0); + } else { + mPopup.setWindowLayoutMode( + mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? + ViewGroup.LayoutParams.MATCH_PARENT : 0, + ViewGroup.LayoutParams.MATCH_PARENT); + } + } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { + heightSpec = height; + } else { + heightSpec = mDropDownHeight; + } + + mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); + + mPopup.update(getAnchorView(), mDropDownHorizontalOffset, + mDropDownVerticalOffset, widthSpec, heightSpec); + } else { + if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { + widthSpec = ViewGroup.LayoutParams.MATCH_PARENT; + } else { + if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { + mPopup.setWidth(getAnchorView().getWidth()); + } else { + mPopup.setWidth(mDropDownWidth); + } + } + + if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + heightSpec = ViewGroup.LayoutParams.MATCH_PARENT; + } else { + if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { + mPopup.setHeight(height); + } else { + mPopup.setHeight(mDropDownHeight); + } + } + + mPopup.setWindowLayoutMode(widthSpec, heightSpec); + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); + + // use outside touchable to dismiss drop down when touching outside of it, so + // only set this if the dropdown is not always visible + mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); + mPopup.setTouchInterceptor(mTouchInterceptor); + mPopup.showAsDropDown(getAnchorView(), + mDropDownHorizontalOffset, mDropDownVerticalOffset); + mDropDownList.setSelection(ListView.INVALID_POSITION); + + if (!mModal || mDropDownList.isInTouchMode()) { + clearListSelection(); + } + if (!mModal) { + mHandler.post(mHideSelector); + } + } + } + + /** + * Dismiss the popup window. + */ + public void dismiss() { + mPopup.dismiss(); + removePromptView(); + mPopup.setContentView(null); + mDropDownList = null; + } + + private void removePromptView() { + if (mPromptView != null) { + final ViewParent parent = mPromptView.getParent(); + if (parent instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) parent; + group.removeView(mPromptView); + } + } + } + + /** + * Control how the popup operates with an input method: one of + * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED}, + * or {@link #INPUT_METHOD_NOT_NEEDED}. + * + *

If the popup is showing, calling this method will take effect only + * the next time the popup is shown or through a manual call to the {@link #show()} + * method.

+ * + * @see #getInputMethodMode() + * @see #show() + */ + public void setInputMethodMode(int mode) { + mPopup.setInputMethodMode(mode); + } + + /** + * Return the current value in {@link #setInputMethodMode(int)}. + * + * @see #setInputMethodMode(int) + */ + public int getInputMethodMode() { + return mPopup.getInputMethodMode(); + } + + /** + * Set the selected position of the list. + * Only valid when {@link #isShowing()} == {@code true}. + * + * @param position List position to set as selected. + */ + public void setSelection(int position) { + DropDownListView list = mDropDownList; + if (isShowing() && list != null) { + list.mListSelectionHidden = false; + list.setSelection(position); + if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) { + list.setItemChecked(position, true); + } + } + } + + /** + * Clear any current list selection. + * Only valid when {@link #isShowing()} == {@code true}. + */ + public void clearListSelection() { + final DropDownListView list = mDropDownList; + if (list != null) { + // WARNING: Please read the comment where mListSelectionHidden is declared + list.mListSelectionHidden = true; + list.hideSelector(); + list.requestLayout(); + } + } + + /** + * @return {@code true} if the popup is currently showing, {@code false} otherwise. + */ + public boolean isShowing() { + return mPopup.isShowing(); + } + + /** + * @return {@code true} if this popup is configured to assume the user does not need + * to interact with the IME while it is showing, {@code false} otherwise. + */ + public boolean isInputMethodNotNeeded() { + return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED; + } + + /** + * Perform an item click operation on the specified list adapter position. + * + * @param position Adapter position for performing the click + * @return true if the click action could be performed, false if not. + * (e.g. if the popup was not showing, this method would return false.) + */ + public boolean performItemClick(int position) { + if (isShowing()) { + if (mItemClickListener != null) { + final DropDownListView list = mDropDownList; + final View child = list.getChildAt(position - list.getFirstVisiblePosition()); + mItemClickListener.onItemClick(list, child, position, child.getId()); + } + return true; + } + return false; + } + + /** + * @return The currently selected item or null if the popup is not showing. + */ + public Object getSelectedItem() { + if (!isShowing()) { + return null; + } + return mDropDownList.getSelectedItem(); + } + + /** + * @return The position of the currently selected item or {@link ListView#INVALID_POSITION} + * if {@link #isShowing()} == {@code false}. + * + * @see ListView#getSelectedItemPosition() + */ + public int getSelectedItemPosition() { + if (!isShowing()) { + return ListView.INVALID_POSITION; + } + return mDropDownList.getSelectedItemPosition(); + } + + /** + * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID} + * if {@link #isShowing()} == {@code false}. + * + * @see ListView#getSelectedItemId() + */ + public long getSelectedItemId() { + if (!isShowing()) { + return ListView.INVALID_ROW_ID; + } + return mDropDownList.getSelectedItemId(); + } + + /** + * @return The View for the currently selected item or null if + * {@link #isShowing()} == {@code false}. + * + * @see ListView#getSelectedView() + */ + public View getSelectedView() { + if (!isShowing()) { + return null; + } + return mDropDownList.getSelectedView(); + } + + /** + * @return The {@link ListView} displayed within the popup window. + * Only valid when {@link #isShowing()} == {@code true}. + */ + public ListView getListView() { + return mDropDownList; + } + + /** + * Filter key down events. By forwarding key up events to this function, + * views using non-modal ListPopupWindow can have it handle key selection of items. + * + * @param keyCode keyCode param passed to the host view's onKeyDown + * @param event event param passed to the host view's onKeyDown + * @return true if the event was handled, false if it was ignored. + * + * @see #setModal(boolean) + */ + public boolean onKeyDown(int keyCode, KeyEvent event) { + // when the drop down is shown, we drive it directly + if (isShowing()) { + // the key events are forwarded to the list in the drop down view + // note that ListView handles space but we don't want that to happen + // also if selection is not currently in the drop down, then don't + // let center or enter presses go there since that would cause it + // to select one of its items + if (keyCode != KeyEvent.KEYCODE_SPACE + && (mDropDownList.getSelectedItemPosition() >= 0 + || (keyCode != KeyEvent.KEYCODE_ENTER + && keyCode != KeyEvent.KEYCODE_DPAD_CENTER))) { + int curIndex = mDropDownList.getSelectedItemPosition(); + boolean consumed; + + final boolean below = !mPopup.isAboveAnchor(); + + final ListAdapter adapter = mAdapter; + + boolean allEnabled; + int firstItem = Integer.MAX_VALUE; + int lastItem = Integer.MIN_VALUE; + + if (adapter != null) { + allEnabled = adapter.areAllItemsEnabled(); + firstItem = allEnabled ? 0 : + mDropDownList.lookForSelectablePosition(0, true); + lastItem = allEnabled ? adapter.getCount() - 1 : + mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false); + } + + if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) || + (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) { + // When the selection is at the top, we block the key + // event to prevent focus from moving. + clearListSelection(); + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); + show(); + return true; + } else { + // WARNING: Please read the comment where mListSelectionHidden + // is declared + mDropDownList.mListSelectionHidden = false; + } + + consumed = mDropDownList.onKeyDown(keyCode, event); + if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed); + + if (consumed) { + // If it handled the key event, then the user is + // navigating in the list, so we should put it in front. + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); + // Here's a little trick we need to do to make sure that + // the list view is actually showing its focus indicator, + // by ensuring it has focus and getting its window out + // of touch mode. + mDropDownList.requestFocusFromTouch(); + show(); + + switch (keyCode) { + // avoid passing the focus from the text view to the + // next component + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_UP: + return true; + } + } else { + if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + // when the selection is at the bottom, we block the + // event to avoid going to the next focusable widget + if (curIndex == lastItem) { + return true; + } + } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP && + curIndex == firstItem) { + return true; + } + } + } + } + + return false; + } + + /** + * Filter key down events. By forwarding key up events to this function, + * views using non-modal ListPopupWindow can have it handle key selection of items. + * + * @param keyCode keyCode param passed to the host view's onKeyUp + * @param event event param passed to the host view's onKeyUp + * @return true if the event was handled, false if it was ignored. + * + * @see #setModal(boolean) + */ + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) { + boolean consumed = mDropDownList.onKeyUp(keyCode, event); + if (consumed) { + switch (keyCode) { + // if the list accepts the key events and the key event + // was a click, the text view gets the selected item + // from the drop down as its content + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + dismiss(); + break; + } + } + return consumed; + } + return false; + } + + /** + * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)} + * events to this function, views using ListPopupWindow can have it dismiss the popup + * when the back key is pressed. + * + * @param keyCode keyCode param passed to the host view's onKeyPreIme + * @param event event param passed to the host view's onKeyPreIme + * @return true if the event was handled, false if it was ignored. + * + * @see #setModal(boolean) + */ + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) { + // special case for the back key, we do not even try to send it + // to the drop down list but instead, consume it immediately + final View anchorView = mDropDownAnchorView; + if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { + anchorView.getKeyDispatcherState().startTracking(event, this); + return true; + } else if (event.getAction() == KeyEvent.ACTION_UP) { + anchorView.getKeyDispatcherState().handleUpEvent(event); + if (event.isTracking() && !event.isCanceled()) { + dismiss(); + return true; + } + } + } + return false; + } + + /** + *

Builds the popup window's content and returns the height the popup + * should have. Returns -1 when the content already exists.

+ * + * @return the content's height or -1 if content already exists + */ + private int buildDropDown() { + ViewGroup dropDownView; + int otherHeights = 0; + + if (mDropDownList == null) { + Context context = mContext; + + /** + * This Runnable exists for the sole purpose of checking if the view layout has got + * completed and if so call showDropDown to display the drop down. This is used to show + * the drop down as soon as possible after user opens up the search dialog, without + * waiting for the normal UI pipeline to do it's job which is slower than this method. + */ + mShowDropDownRunnable = new Runnable() { + public void run() { + // View layout should be all done before displaying the drop down. + View view = getAnchorView(); + if (view != null && view.getWindowToken() != null) { + show(); + } + } + }; + + mDropDownList = new DropDownListView(context, !mModal); + if (mDropDownListHighlight != null) { + mDropDownList.setSelector(mDropDownListHighlight); + } + mDropDownList.setAdapter(mAdapter); + mDropDownList.setVerticalFadingEdgeEnabled(true); + mDropDownList.setOnItemClickListener(mItemClickListener); + mDropDownList.setFocusable(true); + mDropDownList.setFocusableInTouchMode(true); + mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + public void onItemSelected(AdapterView parent, View view, + int position, long id) { + + if (position != -1) { + DropDownListView dropDownList = mDropDownList; + + if (dropDownList != null) { + dropDownList.mListSelectionHidden = false; + } + } + } + + public void onNothingSelected(AdapterView parent) { + } + }); + mDropDownList.setOnScrollListener(mScrollListener); + + if (mItemSelectedListener != null) { + mDropDownList.setOnItemSelectedListener(mItemSelectedListener); + } + + dropDownView = mDropDownList; + + View hintView = mPromptView; + if (hintView != null) { + // if an hint has been specified, we accomodate more space for it and + // add a text view in the drop down menu, at the bottom of the list + LinearLayout hintContainer = new LinearLayout(context); + hintContainer.setOrientation(LinearLayout.VERTICAL); + + LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f + ); + + switch (mPromptPosition) { + case POSITION_PROMPT_BELOW: + hintContainer.addView(dropDownView, hintParams); + hintContainer.addView(hintView); + break; + + case POSITION_PROMPT_ABOVE: + hintContainer.addView(hintView); + hintContainer.addView(dropDownView, hintParams); + break; + + default: + Log.e(TAG, "Invalid hint position " + mPromptPosition); + break; + } + + // measure the hint's height to find how much more vertical space + // we need to add to the drop down's height + int widthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.AT_MOST); + int heightSpec = MeasureSpec.UNSPECIFIED; + hintView.measure(widthSpec, heightSpec); + + hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams(); + otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin + + hintParams.bottomMargin; + + dropDownView = hintContainer; + } + + mPopup.setContentView(dropDownView); + } else { + dropDownView = (ViewGroup) mPopup.getContentView(); + final View view = mPromptView; + if (view != null) { + LinearLayout.LayoutParams hintParams = + (LinearLayout.LayoutParams) view.getLayoutParams(); + otherHeights = view.getMeasuredHeight() + hintParams.topMargin + + hintParams.bottomMargin; + } + } + + // Max height available on the screen for a popup. + boolean ignoreBottomDecorations = + mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; + final int maxHeight = mPopup.getMaxAvailableHeight( + getAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations); + + // getMaxAvailableHeight() subtracts the padding, so we put it back, + // to get the available height for the whole window + int padding = 0; + Drawable background = mPopup.getBackground(); + if (background != null) { + background.getPadding(mTempRect); + padding = mTempRect.top + mTempRect.bottom; + } + + if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + return maxHeight + padding; + } + + final int listContent = mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED, + 0, ListView.NO_POSITION, maxHeight - otherHeights, 2); + // add padding only if the list has items in it, that way we don't show + // the popup if it is not needed + if (listContent > 0) otherHeights += padding; + + return listContent + otherHeights; + } + + /** + *

Wrapper class for a ListView. This wrapper can hijack the focus to + * make sure the list uses the appropriate drawables and states when + * displayed on screen within a drop down. The focus is never actually + * passed to the drop down in this mode; the list only looks focused.

+ */ + private static class DropDownListView extends ListView { + private static final String TAG = ListPopupWindow.TAG + ".DropDownListView"; + /* + * WARNING: This is a workaround for a touch mode issue. + * + * Touch mode is propagated lazily to windows. This causes problems in + * the following scenario: + * - Type something in the AutoCompleteTextView and get some results + * - Move down with the d-pad to select an item in the list + * - Move up with the d-pad until the selection disappears + * - Type more text in the AutoCompleteTextView *using the soft keyboard* + * and get new results; you are now in touch mode + * - The selection comes back on the first item in the list, even though + * the list is supposed to be in touch mode + * + * Using the soft keyboard triggers the touch mode change but that change + * is propagated to our window only after the first list layout, therefore + * after the list attempts to resurrect the selection. + * + * The trick to work around this issue is to pretend the list is in touch + * mode when we know that the selection should not appear, that is when + * we know the user moved the selection away from the list. + * + * This boolean is set to true whenever we explicitly hide the list's + * selection and reset to false whenever we know the user moved the + * selection back to the list. + * + * When this boolean is true, isInTouchMode() returns true, otherwise it + * returns super.isInTouchMode(). + */ + private boolean mListSelectionHidden; + + /** + * True if this wrapper should fake focus. + */ + private boolean mHijackFocus; + + /** + *

Creates a new list view wrapper.

+ * + * @param context this view's context + */ + public DropDownListView(Context context, boolean hijackFocus) { + super(context, null, com.android.internal.R.attr.dropDownListViewStyle); + mHijackFocus = hijackFocus; + } + + /** + *

Avoids jarring scrolling effect by ensuring that list elements + * made of a text view fit on a single line.

+ * + * @param position the item index in the list to get a view for + * @return the view for the specified item + */ + @Override + View obtainView(int position, boolean[] isScrap) { + View view = super.obtainView(position, isScrap); + + if (view instanceof TextView) { + ((TextView) view).setHorizontallyScrolling(true); + } + + return view; + } + + @Override + public boolean isInTouchMode() { + // WARNING: Please read the comment where mListSelectionHidden is declared + return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode(); + } + + /** + *

Returns the focus state in the drop down.

+ * + * @return true always if hijacking focus + */ + @Override + public boolean hasWindowFocus() { + return mHijackFocus || super.hasWindowFocus(); + } + + /** + *

Returns the focus state in the drop down.

+ * + * @return true always if hijacking focus + */ + @Override + public boolean isFocused() { + return mHijackFocus || super.isFocused(); + } + + /** + *

Returns the focus state in the drop down.

+ * + * @return true always if hijacking focus + */ + @Override + public boolean hasFocus() { + return mHijackFocus || super.hasFocus(); + } + } + + private class PopupDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + if (isShowing()) { + // Resize the popup to fit new content + show(); + } + } + + @Override + public void onInvalidated() { + dismiss(); + } + } + + private class ListSelectorHider implements Runnable { + public void run() { + clearListSelection(); + } + } + + private class ResizePopupRunnable implements Runnable { + public void run() { + mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); + show(); + } + } + + private class PopupTouchInterceptor implements OnTouchListener { + public boolean onTouch(View v, MotionEvent event) { + final int action = event.getAction(); + final int x = (int) event.getX(); + final int y = (int) event.getY(); + + if (action == MotionEvent.ACTION_DOWN && + mPopup != null && mPopup.isShowing() && + (x >= 0 && x < getWidth() && y >= 0 && y < getHeight())) { + mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT); + } else if (action == MotionEvent.ACTION_UP) { + mHandler.removeCallbacks(mResizePopupRunnable); + } + return false; + } + } + + private class PopupScrollListener implements ListView.OnScrollListener { + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + + } + + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (scrollState == SCROLL_STATE_TOUCH_SCROLL && + !isInputMethodNotNeeded() && mPopup.getContentView() != null) { + mHandler.removeCallbacks(mResizePopupRunnable); + mResizePopupRunnable.run(); + } + } + } +} diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java index 0378328563562..d404ce70a9aac 100644 --- a/core/java/android/widget/PopupWindow.java +++ b/core/java/android/widget/PopupWindow.java @@ -16,27 +16,28 @@ package android.widget; -import com.android.internal.R; +import java.lang.ref.WeakReference; import android.content.Context; import android.content.res.TypedArray; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; -import android.view.WindowManager; -import android.view.Gravity; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.view.ViewTreeObserver.OnScrollChangedListener; -import android.view.View.OnTouchListener; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.StateListDrawable; import android.os.IBinder; import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.WindowManager; +import android.view.View.OnTouchListener; +import android.view.ViewTreeObserver.OnScrollChangedListener; -import java.lang.ref.WeakReference; +import com.android.internal.R; /** *

A popup window that can be used to display an arbitrary view. The popup @@ -157,12 +158,21 @@ public class PopupWindow { *

The popup does provide a background.

*/ public PopupWindow(Context context, AttributeSet attrs, int defStyle) { + this(context, attrs, defStyle, 0); + } + + /** + *

Create a new, empty, non focusable popup window of dimension (0,0).

+ * + *

The popup does not provide a background.

+ */ + public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { mContext = context; mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); TypedArray a = context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.PopupWindow, defStyle, 0); + attrs, com.android.internal.R.styleable.PopupWindow, defStyleAttr, defStyleRes); mBackground = a.getDrawable(R.styleable.PopupWindow_popupBackground); @@ -1315,6 +1325,7 @@ public class PopupWindow { } private class PopupViewContainer extends FrameLayout { + private static final String TAG = "PopupWindow.PopupViewContainer"; public PopupViewContainer(Context context) { super(context); diff --git a/core/java/android/widget/Spinner.java b/core/java/android/widget/Spinner.java index 2f6dd1e853e4e..feb4211c93083 100644 --- a/core/java/android/widget/Spinner.java +++ b/core/java/android/widget/Spinner.java @@ -24,6 +24,7 @@ import android.content.DialogInterface.OnClickListener; import android.content.res.TypedArray; import android.database.DataSetObserver; import android.util.AttributeSet; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -37,10 +38,20 @@ import android.view.ViewGroup; */ @Widget public class Spinner extends AbsSpinner implements OnClickListener { + private static final String TAG = "Spinner"; + + /** + * Use a dialog window for selecting spinner options. + */ + public static final int MODE_DIALOG = 0; + + /** + * Use a dropdown anchored to the Spinner for selecting spinner options. + */ + public static final int MODE_DROPDOWN = 1; + + private SpinnerPopup mPopup; - private CharSequence mPrompt; - private AlertDialog mPopup; - public Spinner(Context context) { this(context, null); } @@ -55,10 +66,43 @@ public class Spinner extends AbsSpinner implements OnClickListener { TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Spinner, defStyle, 0); - mPrompt = a.getString(com.android.internal.R.styleable.Spinner_prompt); + final int mode = a.getInt(com.android.internal.R.styleable.Spinner_spinnerMode, + MODE_DIALOG); + + switch (mode) { + case MODE_DIALOG: { + mPopup = new DialogPopup(); + break; + } + + case MODE_DROPDOWN: { + final int hintResource = a.getResourceId( + com.android.internal.R.styleable.Spinner_popupPromptView, 0); + + DropdownPopup popup = new DropdownPopup(context, attrs, defStyle, hintResource); + + popup.setBackgroundDrawable(a.getDrawable( + com.android.internal.R.styleable.Spinner_popupBackground)); + popup.setVerticalOffset(a.getDimensionPixelOffset( + com.android.internal.R.styleable.Spinner_dropDownVerticalOffset, 0)); + popup.setHorizontalOffset(a.getDimensionPixelOffset( + com.android.internal.R.styleable.Spinner_dropDownHorizontalOffset, 0)); + + mPopup = popup; + break; + } + } + + mPopup.setPromptText(a.getString(com.android.internal.R.styleable.Spinner_prompt)); a.recycle(); } + + @Override + public void setAdapter(SpinnerAdapter adapter) { + super.setAdapter(adapter); + mPopup.setAdapter(new DropDownAdapter(adapter)); + } @Override public int getBaseline() { @@ -246,15 +290,10 @@ public class Spinner extends AbsSpinner implements OnClickListener { if (!handled) { handled = true; - Context context = getContext(); - - final DropDownAdapter adapter = new DropDownAdapter(getAdapter()); - AlertDialog.Builder builder = new AlertDialog.Builder(context); - if (mPrompt != null) { - builder.setTitle(mPrompt); + if (!mPopup.isShowing()) { + mPopup.show(); } - mPopup = builder.setSingleChoiceItems(adapter, getSelectedItemPosition(), this).show(); } return handled; @@ -271,7 +310,7 @@ public class Spinner extends AbsSpinner implements OnClickListener { * @param prompt the prompt to set */ public void setPrompt(CharSequence prompt) { - mPrompt = prompt; + mPopup.setPromptText(prompt); } /** @@ -279,16 +318,42 @@ public class Spinner extends AbsSpinner implements OnClickListener { * @param promptId the resource ID of the prompt to display when the dialog is shown */ public void setPromptId(int promptId) { - mPrompt = getContext().getText(promptId); + setPrompt(getContext().getText(promptId)); } /** * @return The prompt to display when the dialog is shown */ public CharSequence getPrompt() { - return mPrompt; + return mPopup.getHintText(); } + /* + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (mPopup.onKeyDown(keyCode, event)) { + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (mPopup.onKeyUp(keyCode, event)) { + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + if (mPopup.onKeyPreIme(keyCode, event)) { + return true; + } + return super.onKeyPreIme(keyCode, event); + } + */ + /** *

Wrapper class for an Adapter. Transforms the embedded Adapter instance * into a ListAdapter.

@@ -384,4 +449,123 @@ public class Spinner extends AbsSpinner implements OnClickListener { return getCount() == 0; } } + + /** + * Implements some sort of popup selection interface for selecting a spinner option. + * Allows for different spinner modes. + */ + private interface SpinnerPopup { + public void setAdapter(ListAdapter adapter); + + /** + * Show the popup + */ + public void show(); + + /** + * Dismiss the popup + */ + public void dismiss(); + + /** + * @return true if the popup is showing, false otherwise. + */ + public boolean isShowing(); + + /** + * Set hint text to be displayed to the user. This should provide + * a description of the choice being made. + * @param hintText Hint text to set. + */ + public void setPromptText(CharSequence hintText); + public CharSequence getHintText(); + } + + private class DialogPopup implements SpinnerPopup, DialogInterface.OnClickListener { + private AlertDialog mPopup; + private ListAdapter mListAdapter; + private CharSequence mPrompt; + + public void dismiss() { + mPopup.dismiss(); + mPopup = null; + } + + public boolean isShowing() { + return mPopup != null ? mPopup.isShowing() : false; + } + + public void setAdapter(ListAdapter adapter) { + mListAdapter = adapter; + } + + public void setPromptText(CharSequence hintText) { + mPrompt = hintText; + } + + public CharSequence getHintText() { + return mPrompt; + } + + public void show() { + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + if (mPrompt != null) { + builder.setTitle(mPrompt); + } + mPopup = builder.setSingleChoiceItems(mListAdapter, + getSelectedItemPosition(), this).show(); + } + + public void onClick(DialogInterface dialog, int which) { + setSelection(which); + dismiss(); + } + } + + private class DropdownPopup extends ListPopupWindow implements SpinnerPopup { + private CharSequence mHintText; + private TextView mHintView; + private int mHintResource; + + public DropdownPopup(Context context, AttributeSet attrs, + int defStyleRes, int hintResource) { + super(context, attrs, 0, defStyleRes); + + mHintResource = hintResource; + + setAnchorView(Spinner.this); + setModal(true); + setPromptPosition(POSITION_PROMPT_BELOW); + setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView parent, View v, int position, long id) { + Spinner.this.setSelection(position); + dismiss(); + } + }); + } + + public CharSequence getHintText() { + return mHintText; + } + + public void setPromptText(CharSequence hintText) { + mHintText = hintText; + if (mHintView != null) { + mHintView.setText(hintText); + } + } + + public void show() { + if (mHintView == null) { + final TextView textView = (TextView) LayoutInflater.from(getContext()).inflate( + mHintResource, null).findViewById(com.android.internal.R.id.text1); + textView.setText(mHintText); + setPromptView(textView); + mHintView = textView; + } + super.show(); + getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE); + setSelection(Spinner.this.getSelectedItemPosition()); + } + } } diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index e7b83c7fc2645..af8d91c12b97e 100755 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -2207,6 +2207,30 @@ + + + + + + + + + + + + + + + + + + + diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 1aa55e22a4959..7ef842f5bef9b 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -1286,5 +1286,6 @@ + diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml index c90770c5ed62e..4f74413f40f3c 100644 --- a/core/res/res/values/styles.xml +++ b/core/res/res/values/styles.xml @@ -451,6 +451,18 @@ + +