Java tutorial
package com.arlib.floatingsearchview; /** * Copyright (C) 2015 Ari C. * <p/> * 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 * <p/> * http://www.apache.org/licenses/LICENSE-2.0 * <p/> * 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. */ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.app.Activity; import android.content.Context; import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v4.view.GravityCompat; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewPropertyAnimatorListenerAdapter; import android.support.v4.view.ViewPropertyAnimatorUpdateListener; import android.support.v4.widget.DrawerLayout; import android.support.v7.graphics.drawable.DrawerArrowDrawable; import android.support.v7.view.menu.MenuBuilder; import android.support.v7.widget.CardView; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.inputmethod.EditorInfo; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; import com.arlib.floatingsearchview.suggestions.SearchSuggestionsAdapter; import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion; import com.arlib.floatingsearchview.util.Util; import com.arlib.floatingsearchview.util.adapter.GestureDetectorListenerAdapter; import com.arlib.floatingsearchview.util.adapter.OnItemTouchListenerAdapter; import com.arlib.floatingsearchview.util.adapter.TextWatcherAdapter; import com.arlib.floatingsearchview.util.view.MenuView; import com.arlib.floatingsearchview.util.view.SearchInputView; import com.bartoszlipinski.viewpropertyobjectanimator.ViewPropertyObjectAnimator; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * A search UI widget that implements a floating search box also called persistent * search. */ public class FloatingSearchView extends FrameLayout { private final static String TAG = FloatingSearchView.class.getSimpleName(); //The CardView's top or bottom height used for its shadow private final static int CARD_VIEW_TOP_BOTTOM_SHADOW_HEIGHT = 3; //The CardView's (default) corner radius height private final static int CARD_VIEW_CORNERS_HEIGHT = 2; private final static int CARD_VIEW_CORNERS_AND_TOP_BOTTOM_SHADOW_HEIGHT = CARD_VIEW_TOP_BOTTOM_SHADOW_HEIGHT + CARD_VIEW_CORNERS_HEIGHT; private final static long CLEAR_BTN_FADE_ANIM_DURATION = 500; private final static int CLEAR_BTN_WIDTH_DP = 48; private final static int LEFT_MENU_WIDTH_AND_MARGIN_START_DP = 52; private final static float MENU_BUTTON_PROGRESS_ARROW = 1.0f; private final static float MENU_BUTTON_PROGRESS_HAMBURGER = 0.0f; private final static int BACKGROUND_DRAWABLE_ALPHA_SEARCH_FOCUSED = 150; private final static int BACKGROUND_DRAWABLE_ALPHA_SEARCH_NOT_FOCUSED = 0; private final static int BACKGROUND_FADE_ANIM_DURATION = 250; private final static int MENU_ICON_ANIM_DURATION = 250; private final static Interpolator SUGGEST_ITEM_ADD_ANIM_INTERPOLATOR = new LinearInterpolator(); public final static int LEFT_ACTION_MODE_SHOW_HAMBURGER = 1; public final static int LEFT_ACTION_MODE_SHOW_SEARCH = 2; public final static int LEFT_ACTION_MODE_SHOW_HOME = 3; public final static int LEFT_ACTION_MODE_NO_LEFT_ACTION = 4; private final static int LEFT_ACTION_MODE_NOT_SET = -1; @Retention(RetentionPolicy.SOURCE) @IntDef({ LEFT_ACTION_MODE_SHOW_HAMBURGER, LEFT_ACTION_MODE_SHOW_SEARCH, LEFT_ACTION_MODE_SHOW_HOME, LEFT_ACTION_MODE_NO_LEFT_ACTION, LEFT_ACTION_MODE_NOT_SET }) public @interface LeftActionMode { } @LeftActionMode private final static int ATTRS_SEARCH_BAR_LEFT_ACTION_MODE_DEFAULT = LEFT_ACTION_MODE_NO_LEFT_ACTION; private final static boolean ATTRS_SHOW_MOVE_UP_SUGGESTION_DEFAULT = false; private final static boolean ATTRS_DISMISS_ON_OUTSIDE_TOUCH_DEFAULT = true; private final static boolean ATTRS_DISMISS_ON_KEYBOARD_DISMISS_DEFAULT = false; private final static boolean ATTRS_SEARCH_BAR_SHOW_SEARCH_KEY_DEFAULT = true; private final static int ATTRS_QUERY_TEXT_SIZE_SP_DEFAULT = 18; private final static int ATTRS_SUGGESTION_TEXT_SIZE_SP_DEFAULT = 18; private final static boolean ATTRS_SHOW_DIM_BACKGROUND_DEFAULT = true; private final static int ATTRS_SUGGESTION_ANIM_DURATION_DEFAULT = 250; private final static int ATTRS_SEARCH_BAR_MARGIN_DEFAULT = 0; private Activity mHostActivity; private View mMainLayout; private Drawable mBackgroundDrawable; private boolean mDimBackground; private boolean mDismissOnOutsideTouch = true; private boolean mIsFocused; private OnFocusChangeListener mFocusChangeListener; private CardView mQuerySection; private OnSearchListener mSearchListener; private SearchInputView mSearchInput; private int mQueryTextSize; private boolean mCloseSearchOnSofteKeyboardDismiss; private String mTitleText; private boolean mIsTitleSet; private int mSearchInputTextColor = -1; private int mSearchInputHintColor = -1; private View mSearchInputParent; private String mOldQuery = ""; private OnQueryChangeListener mQueryListener; private ImageView mLeftAction; private OnLeftMenuClickListener mOnMenuClickListener; private OnHomeActionClickListener mOnHomeActionClickListener; private ProgressBar mSearchProgress; private DrawerArrowDrawable mMenuBtnDrawable; private Drawable mIconBackArrow; private Drawable mIconSearch; @LeftActionMode int mLeftActionMode = LEFT_ACTION_MODE_NOT_SET; private int mLeftActionIconColor; private String mSearchHint; private boolean mShowSearchKey; private boolean mMenuOpen = false; private MenuView mMenuView; private int mMenuId = -1; private int mActionMenuItemColor; private int mOverflowIconColor; private OnMenuItemClickListener mActionMenuItemListener; private ImageView mClearButton; private int mClearBtnColor; private Drawable mIconClear; private int mBackgroundColor; private boolean mSkipQueryFocusChangeEvent; private boolean mSkipTextChangeEvent; private View mDivider; private int mDividerColor; private RelativeLayout mSuggestionsSection; private View mSuggestionListContainer; private RecyclerView mSuggestionsList; private int mSuggestionTextColor = -1; private int mSuggestionRightIconColor; private SearchSuggestionsAdapter mSuggestionsAdapter; private SearchSuggestionsAdapter.OnBindSuggestionCallback mOnBindSuggestionCallback; private int mSuggestionsTextSizePx; private boolean mIsInitialLayout = true; private boolean mIsSuggestionsSectionHeightSet; private boolean mShowMoveUpSuggestion = ATTRS_SHOW_MOVE_UP_SUGGESTION_DEFAULT; private OnSuggestionsListHeightChanged mOnSuggestionsListHeightChanged; private long mSuggestionSectionAnimDuration; private OnClearSearchActionListener mOnClearSearchActionListener; //An interface for implementing a listener that will get notified when the suggestions //section's height is set. This is to be used internally only. private interface OnSuggestionSecHeightSetListener { void onSuggestionSecHeightSet(); } private OnSuggestionSecHeightSetListener mSuggestionSecHeightListener; /** * Interface for implementing a listener to listen to * changes in the suggestion list height that occur when the list is expands/shrinks * following calls to {@link FloatingSearchView#swapSuggestions(List)} */ public interface OnSuggestionsListHeightChanged { void onSuggestionsListHeightChanged(float newHeight); } /** * Interface for implementing a listener to listen * to state changes in the query text. */ public interface OnQueryChangeListener { /** * Called when the query has changed. It will * be invoked when one or more characters in the * query was changed. * * @param oldQuery the previous query * @param newQuery the new query */ void onSearchTextChanged(String oldQuery, String newQuery); } /** * Interface for implementing a listener to listen * to when the current search has completed. */ public interface OnSearchListener { /** * Called when a suggestion was clicked indicating * that the current search has completed. * * @param searchSuggestion */ void onSuggestionClicked(SearchSuggestion searchSuggestion); /** * Called when the current search has completed * as a result of pressing search key in the keyboard. * <p/> * Note: This will only get called if * {@link FloatingSearchView#setShowSearchKey(boolean)}} is set to true. * * @param currentQuery the text that is currently set in the query TextView */ void onSearchAction(String currentQuery); } /** * Interface for implementing a callback to be * invoked when the left menu (navigation menu) is * clicked. * <p/> * Note: This is only relevant when leftActionMode is * set to {@value #LEFT_ACTION_MODE_SHOW_HAMBURGER} */ public interface OnLeftMenuClickListener { /** * Called when the menu button was * clicked and the menu's state is now opened. */ void onMenuOpened(); /** * Called when the back button was * clicked and the menu's state is now closed. */ void onMenuClosed(); } /** * Interface for implementing a callback to be * invoked when the home action button (the back arrow) * is clicked. * <p/> * Note: This is only relevant when leftActionMode is * set to {@value #LEFT_ACTION_MODE_SHOW_HOME} */ public interface OnHomeActionClickListener { /** * Called when the home button was * clicked. */ void onHomeClicked(); } /** * Interface for implementing a listener to listen * when an item in the action (the item can be presented as an action * ,or as a menu item in the overflow menu) menu has been selected. */ public interface OnMenuItemClickListener { /** * Called when a menu item in has been * selected. * * @param item the selected menu item. */ void onActionMenuItemSelected(MenuItem item); } /** * Interface for implementing a listener to listen * to for focus state changes. */ public interface OnFocusChangeListener { /** * Called when the search bar has gained focus * and listeners are now active. */ void onFocus(); /** * Called when the search bar has lost focus * and listeners are no more active. */ void onFocusCleared(); } /** * Interface for implementing a callback to be * invoked when the clear search text action button * (the x to the right of the text) is clicked. */ public interface OnClearSearchActionListener { /** * Called when the clear search text button * was clicked. */ void onClearSearchClicked(); } public FloatingSearchView(Context context) { this(context, null); } public FloatingSearchView(Context context, AttributeSet attrs) { super(context, attrs); init(attrs); } private void init(AttributeSet attrs) { mHostActivity = Util.getHostActivity(getContext()); LayoutInflater layoutInflater = (LayoutInflater) getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); mMainLayout = inflate(getContext(), R.layout.floating_search_layout, this); mBackgroundDrawable = new ColorDrawable(Color.BLACK); mQuerySection = (CardView) findViewById(R.id.search_query_section); mClearButton = (ImageView) findViewById(R.id.clear_btn); mSearchInput = (SearchInputView) findViewById(R.id.search_bar_text); mSearchInputParent = findViewById(R.id.search_input_parent); mLeftAction = (ImageView) findViewById(R.id.left_action); mSearchProgress = (ProgressBar) findViewById(R.id.search_bar_search_progress); initDrawables(); mClearButton.setImageDrawable(mIconClear); mMenuView = (MenuView) findViewById(R.id.menu_view); mDivider = findViewById(R.id.divider); mSuggestionsSection = (RelativeLayout) findViewById(R.id.search_suggestions_section); mSuggestionListContainer = findViewById(R.id.suggestions_list_container); mSuggestionsList = (RecyclerView) findViewById(R.id.suggestions_list); setupViews(attrs); } private void initDrawables() { mMenuBtnDrawable = new DrawerArrowDrawable(getContext()); mIconClear = Util.getWrappedDrawable(getContext(), R.drawable.ic_clear_black_24dp); mIconBackArrow = Util.getWrappedDrawable(getContext(), R.drawable.ic_arrow_back_black_24dp); mIconSearch = Util.getWrappedDrawable(getContext(), R.drawable.ic_search_black_24dp); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (mIsInitialLayout) { //we need to add 5dp to the mSuggestionsSection because we are //going to move it up by 5dp in order to cover the search bar's //shadow padding and rounded corners. We also need to add an additional 10dp to //mSuggestionsSection in order to hide mSuggestionListContainer's //rounded corners and shadow for both, top and bottom. int addedHeight = 3 * Util.dpToPx(CARD_VIEW_CORNERS_AND_TOP_BOTTOM_SHADOW_HEIGHT); final int finalHeight = mSuggestionsSection.getHeight() + addedHeight; mSuggestionsSection.getLayoutParams().height = finalHeight; mSuggestionsSection.requestLayout(); ViewTreeObserver vto = mSuggestionListContainer.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { if (mSuggestionsSection.getHeight() == finalHeight) { Util.removeGlobalLayoutObserver(mSuggestionListContainer, this); mIsSuggestionsSectionHeightSet = true; moveSuggestListToInitialPos(); if (mSuggestionSecHeightListener != null) { mSuggestionSecHeightListener.onSuggestionSecHeightSet(); mSuggestionSecHeightListener = null; } } } }); mIsInitialLayout = false; refreshDimBackground(); if (isInEditMode()) { inflateOverflowMenu(mMenuId); } } } private void setupViews(AttributeSet attrs) { mSuggestionsSection.setEnabled(false); if (attrs != null) { applyXmlAttributes(attrs); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { setBackground(mBackgroundDrawable); } else { setBackgroundDrawable(mBackgroundDrawable); } setupQueryBar(); if (!isInEditMode()) { setupSuggestionSection(); } } private void applyXmlAttributes(AttributeSet attrs) { TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.FloatingSearchView); try { int searchBarWidth = a.getDimensionPixelSize( R.styleable.FloatingSearchView_floatingSearch_searchBarWidth, ViewGroup.LayoutParams.MATCH_PARENT); mQuerySection.getLayoutParams().width = searchBarWidth; mDivider.getLayoutParams().width = searchBarWidth; mSuggestionListContainer.getLayoutParams().width = searchBarWidth; int searchBarLeftMargin = a.getDimensionPixelSize( R.styleable.FloatingSearchView_floatingSearch_searchBarMarginLeft, ATTRS_SEARCH_BAR_MARGIN_DEFAULT); int searchBarTopMargin = a.getDimensionPixelSize( R.styleable.FloatingSearchView_floatingSearch_searchBarMarginTop, ATTRS_SEARCH_BAR_MARGIN_DEFAULT); int searchBarRightMargin = a.getDimensionPixelSize( R.styleable.FloatingSearchView_floatingSearch_searchBarMarginRight, ATTRS_SEARCH_BAR_MARGIN_DEFAULT); LayoutParams querySectionLP = (LayoutParams) mQuerySection.getLayoutParams(); LayoutParams dividerLP = (LayoutParams) mDivider.getLayoutParams(); LinearLayout.LayoutParams suggestListSectionLP = (LinearLayout.LayoutParams) mSuggestionsSection .getLayoutParams(); int cardPadding = Util.dpToPx(CARD_VIEW_TOP_BOTTOM_SHADOW_HEIGHT); querySectionLP.setMargins(searchBarLeftMargin, searchBarTopMargin, searchBarRightMargin, 0); dividerLP.setMargins(searchBarLeftMargin + cardPadding, 0, searchBarRightMargin + cardPadding, ((MarginLayoutParams) mDivider.getLayoutParams()).bottomMargin); suggestListSectionLP.setMargins(searchBarLeftMargin, 0, searchBarRightMargin, 0); mQuerySection.setLayoutParams(querySectionLP); mDivider.setLayoutParams(dividerLP); mSuggestionsSection.setLayoutParams(suggestListSectionLP); setQueryTextSize( a.getDimensionPixelSize(R.styleable.FloatingSearchView_floatingSearch_searchInputTextSize, ATTRS_QUERY_TEXT_SIZE_SP_DEFAULT)); setSearchHint(a.getString(R.styleable.FloatingSearchView_floatingSearch_searchHint)); setShowSearchKey(a.getBoolean(R.styleable.FloatingSearchView_floatingSearch_showSearchKey, ATTRS_SEARCH_BAR_SHOW_SEARCH_KEY_DEFAULT)); setCloseSearchOnKeyboardDismiss( a.getBoolean(R.styleable.FloatingSearchView_floatingSearch_close_search_on_keyboard_dismiss, ATTRS_DISMISS_ON_KEYBOARD_DISMISS_DEFAULT)); setDismissOnOutsideClick( a.getBoolean(R.styleable.FloatingSearchView_floatingSearch_dismissOnOutsideTouch, ATTRS_DISMISS_ON_OUTSIDE_TOUCH_DEFAULT)); setSuggestionItemTextSize( a.getDimensionPixelSize(R.styleable.FloatingSearchView_floatingSearch_searchSuggestionTextSize, Util.spToPx(ATTRS_SUGGESTION_TEXT_SIZE_SP_DEFAULT))); //noinspection ResourceType mLeftActionMode = a.getInt(R.styleable.FloatingSearchView_floatingSearch_leftActionMode, ATTRS_SEARCH_BAR_LEFT_ACTION_MODE_DEFAULT); if (a.hasValue(R.styleable.FloatingSearchView_floatingSearch_menu)) { mMenuId = a.getResourceId(R.styleable.FloatingSearchView_floatingSearch_menu, -1); } setDimBackground(a.getBoolean(R.styleable.FloatingSearchView_floatingSearch_dimBackground, ATTRS_SHOW_DIM_BACKGROUND_DEFAULT)); setShowMoveUpSuggestion(a.getBoolean(R.styleable.FloatingSearchView_floatingSearch_showMoveSuggestionUp, ATTRS_SHOW_MOVE_UP_SUGGESTION_DEFAULT)); this.mSuggestionSectionAnimDuration = a.getInt( R.styleable.FloatingSearchView_floatingSearch_suggestionsListAnimDuration, ATTRS_SUGGESTION_ANIM_DURATION_DEFAULT); setBackgroundColor(a.getColor(R.styleable.FloatingSearchView_floatingSearch_backgroundColor, Util.getColor(getContext(), R.color.background))); setLeftActionIconColor(a.getColor(R.styleable.FloatingSearchView_floatingSearch_leftActionColor, Util.getColor(getContext(), R.color.left_action_icon))); setActionMenuOverflowColor( a.getColor(R.styleable.FloatingSearchView_floatingSearch_actionMenuOverflowColor, Util.getColor(getContext(), R.color.overflow_icon_color))); setMenuItemIconColor(a.getColor(R.styleable.FloatingSearchView_floatingSearch_menuItemIconColor, Util.getColor(getContext(), R.color.menu_icon_color))); setDividerColor(a.getColor(R.styleable.FloatingSearchView_floatingSearch_dividerColor, Util.getColor(getContext(), R.color.divider))); setClearBtnColor(a.getColor(R.styleable.FloatingSearchView_floatingSearch_clearBtnColor, Util.getColor(getContext(), R.color.clear_btn_color))); int viewTextColor = a.getColor(R.styleable.FloatingSearchView_floatingSearch_viewTextColor, Util.getColor(getContext(), R.color.dark_gray)); setViewTextColor(viewTextColor); setQueryTextColor(a.getColor(R.styleable.FloatingSearchView_floatingSearch_viewSearchInputTextColor, viewTextColor)); setSuggestionsTextColor(a.getColor( R.styleable.FloatingSearchView_floatingSearch_viewSuggestionItemTextColor, viewTextColor)); setHintTextColor(a.getColor(R.styleable.FloatingSearchView_floatingSearch_hintTextColor, Util.getColor(getContext(), R.color.hint_color))); setSuggestionRightIconColor( a.getColor(R.styleable.FloatingSearchView_floatingSearch_suggestionRightIconColor, Util.getColor(getContext(), R.color.gray_active_icon))); } finally { a.recycle(); } } private void setupQueryBar() { mSearchInput.setTextColor(mSearchInputTextColor); mSearchInput.setHintTextColor(mSearchInputHintColor); if (!isInEditMode() && mHostActivity != null) { mHostActivity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); } ViewTreeObserver vto = mQuerySection.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { Util.removeGlobalLayoutObserver(mQuerySection, this); inflateOverflowMenu(mMenuId); } }); mMenuView.setMenuCallback(new MenuBuilder.Callback() { @Override public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) { if (mActionMenuItemListener != null) { mActionMenuItemListener.onActionMenuItemSelected(item); } //todo check if we should care about this return or not return false; } @Override public void onMenuModeChange(MenuBuilder menu) { } }); mMenuView.setOnVisibleWidthChanged(new MenuView.OnVisibleWidthChangedListener() { @Override public void onItemsMenuVisibleWidthChanged(int newVisibleWidth) { handleOnVisibleMenuItemsWidthChanged(newVisibleWidth); } }); mMenuView.setActionIconColor(mActionMenuItemColor); mMenuView.setOverflowColor(mOverflowIconColor); mClearButton.setVisibility(View.INVISIBLE); mClearButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mSearchInput.setText(""); if (mOnClearSearchActionListener != null) { mOnClearSearchActionListener.onClearSearchClicked(); } } }); mSearchInput.addTextChangedListener(new TextWatcherAdapter() { public void onTextChanged(final CharSequence s, int start, int before, int count) { //todo investigate why this is called twice when pressing back on the keyboard if (mSkipTextChangeEvent || !mIsFocused) { mSkipTextChangeEvent = false; } else { if (mSearchInput.getText().toString().length() != 0 && mClearButton.getVisibility() == View.INVISIBLE) { mClearButton.setAlpha(0.0f); mClearButton.setVisibility(View.VISIBLE); ViewCompat.animate(mClearButton).alpha(1.0f).setDuration(CLEAR_BTN_FADE_ANIM_DURATION) .start(); } else if (mSearchInput.getText().toString().length() == 0) { mClearButton.setVisibility(View.INVISIBLE); } if (mQueryListener != null && mIsFocused && !mOldQuery.equals(mSearchInput.getText().toString())) { mQueryListener.onSearchTextChanged(mOldQuery, mSearchInput.getText().toString()); } } mOldQuery = mSearchInput.getText().toString(); } }); mSearchInput.setOnFocusChangeListener(new TextView.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (mSkipQueryFocusChangeEvent) { mSkipQueryFocusChangeEvent = false; } else if (hasFocus != mIsFocused) { setSearchFocusedInternal(hasFocus); } } }); mSearchInput.setOnKeyboardDismissedListener(new SearchInputView.OnKeyboardDismissedListener() { @Override public void onKeyboardDismissed() { if (mCloseSearchOnSofteKeyboardDismiss) { setSearchFocusedInternal(false); } } }); mSearchInput.setOnSearchKeyListener(new SearchInputView.OnKeyboardSearchKeyClickListener() { @Override public void onSearchKeyClicked() { if (mSearchListener != null) { mSearchListener.onSearchAction(getQuery()); } mSkipTextChangeEvent = true; mSkipTextChangeEvent = true; if (mIsTitleSet) { setSearchBarTitle(getQuery()); } else { setSearchText(getQuery()); } setSearchFocusedInternal(false); } }); mLeftAction.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (isSearchBarFocused()) { setSearchFocusedInternal(false); } else { switch (mLeftActionMode) { case LEFT_ACTION_MODE_SHOW_HAMBURGER: toggleLeftMenu(); break; case LEFT_ACTION_MODE_SHOW_SEARCH: setSearchFocusedInternal(true); break; case LEFT_ACTION_MODE_SHOW_HOME: if (mOnHomeActionClickListener != null) { mOnHomeActionClickListener.onHomeClicked(); } break; case LEFT_ACTION_MODE_NO_LEFT_ACTION: //do nothing break; } } } }); refreshLeftIcon(); } //ensures that the end margin of the search input is according to Material specs private void handleOnVisibleMenuItemsWidthChanged(int menuItemsWidth) { if (menuItemsWidth == 0) { mClearButton.setTranslationX(-Util.dpToPx(4)); int paddingRight = Util.dpToPx(4); if (mIsFocused) { paddingRight += Util.dpToPx(CLEAR_BTN_WIDTH_DP); } else { paddingRight += Util.dpToPx(14); } mSearchInput.setPadding(0, 0, paddingRight, 0); } else { mClearButton.setTranslationX(-menuItemsWidth); int paddingRight = menuItemsWidth; if (mIsFocused) { paddingRight += Util.dpToPx(CLEAR_BTN_WIDTH_DP); } mSearchInput.setPadding(0, 0, paddingRight, 0); } } /** * Sets the menu button's color. * * @param color the color to be applied to the * left menu button. */ public void setLeftActionIconColor(int color) { mLeftActionIconColor = color; mMenuBtnDrawable.setColor(color); DrawableCompat.setTint(mIconBackArrow, color); DrawableCompat.setTint(mIconSearch, color); } /** * Sets the clear button's color. * * @param color the color to be applied to the * clear button. */ public void setClearBtnColor(int color) { mClearBtnColor = color; DrawableCompat.setTint(mIconClear, mClearBtnColor); } /** * Sets the action menu icons' color. * * @param color the color to be applied to the * action menu items. */ public void setMenuItemIconColor(int color) { this.mActionMenuItemColor = color; if (mMenuView != null) { mMenuView.setActionIconColor(this.mActionMenuItemColor); } } /** * Sets the action menu overflow icon's color. * * @param color the color to be applied to the * overflow icon. */ public void setActionMenuOverflowColor(int color) { this.mOverflowIconColor = color; if (mMenuView != null) { mMenuView.setOverflowColor(this.mOverflowIconColor); } } /** * Sets the background color of the search * view including the suggestions section. * * @param color the color to be applied to the search bar and * the suggestion section background. */ public void setBackgroundColor(int color) { mBackgroundColor = color; if (mQuerySection != null && mSuggestionsList != null) { mQuerySection.setCardBackgroundColor(color); mSuggestionsList.setBackgroundColor(color); } } /** * Sets the text color of the search * and suggestion text. * * @param color the color to be applied to the search and suggestion * text. */ public void setViewTextColor(int color) { setSuggestionsTextColor(color); setQueryTextColor(color); } /** * Sets the text color of suggestion text. * * @param color */ public void setSuggestionsTextColor(int color) { mSuggestionTextColor = color; if (mSuggestionsAdapter != null) { mSuggestionsAdapter.setTextColor(mSuggestionTextColor); } } /** * Set the duration for the suggestions list expand/collapse * animation. * * @param duration */ public void setSuggestionsAnimDuration(long duration) { this.mSuggestionSectionAnimDuration = duration; } /** * Sets the text color of the search text. * * @param color */ public void setQueryTextColor(int color) { mSearchInputTextColor = color; if (mSearchInput != null) { mSearchInput.setTextColor(mSearchInputTextColor); } } /** * Set the text size of the text in the search box. * * @param sizePx */ public void setQueryTextSize(int sizePx) { this.mQueryTextSize = sizePx; mSearchInput.setTextSize(mQueryTextSize); } /** * Sets the text color of the search * hint. * * @param color the color to be applied to the search hint. */ public void setHintTextColor(int color) { this.mSearchInputHintColor = color; if (mSearchInput != null) { mSearchInput.setHintTextColor(color); } } /** * Sets the color of the search divider that * divides the search section from the suggestions. * * @param color the color to be applied the divider. */ public void setDividerColor(int color) { mDividerColor = color; if (mDivider != null) { mDivider.setBackgroundColor(mDividerColor); } } /** * Set the tint of the suggestion items' right btn (move suggestion to * query) * * @param color */ public void setSuggestionRightIconColor(int color) { this.mSuggestionRightIconColor = color; if (mSuggestionsAdapter != null) { mSuggestionsAdapter.setRightIconColor(this.mSuggestionRightIconColor); } } /** * Set the text size of the suggestion items. * * @param sizePx */ private void setSuggestionItemTextSize(int sizePx) { //todo implement dynamic suggestionTextSize setter and expose method this.mSuggestionsTextSizePx = sizePx; } /** * Set the mode for the left action button. * * @param mode */ public void setLeftActionMode(@LeftActionMode int mode) { mLeftActionMode = mode; refreshLeftIcon(); } private void refreshLeftIcon() { int leftActionWidthAndMarginLeft = Util.dpToPx(LEFT_MENU_WIDTH_AND_MARGIN_START_DP); int queryTranslationX = 0; mLeftAction.setVisibility(VISIBLE); switch (mLeftActionMode) { case LEFT_ACTION_MODE_SHOW_HAMBURGER: mLeftAction.setImageDrawable(mMenuBtnDrawable); mMenuBtnDrawable.setProgress(MENU_BUTTON_PROGRESS_HAMBURGER); break; case LEFT_ACTION_MODE_SHOW_SEARCH: mLeftAction.setImageDrawable(mIconSearch); break; case LEFT_ACTION_MODE_SHOW_HOME: mLeftAction.setImageDrawable(mMenuBtnDrawable); mMenuBtnDrawable.setProgress(MENU_BUTTON_PROGRESS_ARROW); break; case LEFT_ACTION_MODE_NO_LEFT_ACTION: mLeftAction.setVisibility(View.INVISIBLE); queryTranslationX = -leftActionWidthAndMarginLeft; break; } mSearchInputParent.setTranslationX(queryTranslationX); } private void toggleLeftMenu() { if (mMenuOpen) { closeMenu(true); } else { openMenu(true); } } /** * <p/> * Enables clients to directly manipulate * the menu icon's progress. * <p/> * Useful for custom animation/behaviors. * * @param progress the desired progress of the menu * icon's rotation: 0.0 == hamburger * shape, 1.0 == back arrow shape */ public void setMenuIconProgress(float progress) { mMenuBtnDrawable.setProgress(progress); if (progress == 0) { closeMenu(false); } else if (progress == 1.0) { openMenu(false); } } /** * Mimics a menu click that opens the menu. Useful for navigation * drawers when they open as a result of dragging. */ public void openMenu(boolean withAnim) { mMenuOpen = true; openMenuDrawable(mMenuBtnDrawable, withAnim); if (mOnMenuClickListener != null) { mOnMenuClickListener.onMenuOpened(); } } /** * Mimics a menu click that closes. Useful when fo navigation * drawers when they close as a result of selecting and item. * * @param withAnim true, will close the menu button with * the Material animation */ public void closeMenu(boolean withAnim) { mMenuOpen = false; closeMenuDrawable(mMenuBtnDrawable, withAnim); if (mOnMenuClickListener != null) { mOnMenuClickListener.onMenuClosed(); } } /** * Set the hamburger menu to open or closed without * animating hamburger to arrow and without calling listeners. * * @param isOpen */ public void setLeftMenuOpen(boolean isOpen) { mMenuOpen = isOpen; mMenuBtnDrawable.setProgress(isOpen ? 1.0f : 0.0f); } /** * Shows a circular progress on top of the * menu action button. * <p/> * Call hidProgress() * to change back to normal and make the menu * action visible. */ public void showProgress() { mLeftAction.setVisibility(View.GONE); mSearchProgress.setAlpha(0.0f); mSearchProgress.setVisibility(View.VISIBLE); ObjectAnimator.ofFloat(mSearchProgress, "alpha", 0.0f, 1.0f).start(); } /** * Hides the progress bar after * a prior call to showProgress() */ public void hideProgress() { mSearchProgress.setVisibility(View.GONE); mLeftAction.setAlpha(0.0f); mLeftAction.setVisibility(View.VISIBLE); ObjectAnimator.ofFloat(mLeftAction, "alpha", 0.0f, 1.0f).start(); } /** * Inflates the menu items from * an xml resource. * * @param menuId a menu xml resource reference */ public void inflateOverflowMenu(int menuId) { mMenuId = menuId; mMenuView.reset(menuId, actionMenuAvailWidth()); if (mIsFocused) { mMenuView.hideIfRoomItems(false); } } private int actionMenuAvailWidth() { if (isInEditMode()) { return mQuerySection.getMeasuredWidth() / 2; } return mQuerySection.getWidth() / 2; } /** * Set a hint that will appear in the * search input. Default hint is R.string.abc_search_hint * which is "search..." (when device language is set to english) * * @param searchHint */ public void setSearchHint(String searchHint) { mSearchHint = searchHint != null ? searchHint : getResources().getString(R.string.abc_search_hint); mSearchInput.setHint(mSearchHint); } /** * Sets whether the the button with the search icon * will appear in the soft-keyboard or not. * * @param show to show the search button in * the soft-keyboard. */ public void setShowSearchKey(boolean show) { mShowSearchKey = show; if (show) { mSearchInput.setImeOptions(EditorInfo.IME_ACTION_SEARCH); } else { mSearchInput.setImeOptions(EditorInfo.IME_ACTION_NONE); } } /** * Sets whether the search will lose focus when the softkeyboard * gets closed from a back press * * @param closeSearchOnKeyboardDismiss */ public void setCloseSearchOnKeyboardDismiss(boolean closeSearchOnKeyboardDismiss) { this.mCloseSearchOnSofteKeyboardDismiss = closeSearchOnKeyboardDismiss; } /** * Set whether a touch outside of the * search bar's bounds will cause the search bar to * loos focus. * * @param enable true to dismiss on outside touch, false otherwise. */ public void setDismissOnOutsideClick(boolean enable) { mDismissOnOutsideTouch = enable; mSuggestionsSection.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { //todo check if this is called twice if (mDismissOnOutsideTouch && mIsFocused) { setSearchFocusedInternal(false); } return true; } }); } /** * Sets whether a dim background will show when the search is focused * * @param dimEnabled True to show dim */ public void setDimBackground(boolean dimEnabled) { this.mDimBackground = dimEnabled; refreshDimBackground(); } private void refreshDimBackground() { if (this.mDimBackground && mIsFocused) { mBackgroundDrawable.setAlpha(BACKGROUND_DRAWABLE_ALPHA_SEARCH_FOCUSED); } else { mBackgroundDrawable.setAlpha(BACKGROUND_DRAWABLE_ALPHA_SEARCH_NOT_FOCUSED); } } /** * Sets the arrow up of suggestion items to be enabled and visible or * disabled and invisible. * * @param show */ public void setShowMoveUpSuggestion(boolean show) { mShowMoveUpSuggestion = show; refreshShowMoveUpSuggestion(); } private void refreshShowMoveUpSuggestion() { if (mSuggestionsAdapter != null) { mSuggestionsAdapter.setShowMoveUpIcon(mShowMoveUpSuggestion); } } /** * Wrapper implementation for EditText.setFocusable(boolean focusable) * * @param focusable true, to make search focus when * clicked. */ public void setSearchFocusable(boolean focusable) { mSearchInput.setFocusable(focusable); mSearchInput.setFocusableInTouchMode(focusable); } /** * Sets the title for the search bar. * <p/> * Note that after the title is set, when * the search gains focus, the title will be replaced * by the search hint. * * @param title the title to be shown when search * is not focused */ public void setSearchBarTitle(CharSequence title) { this.mTitleText = title.toString(); mIsTitleSet = true; mSearchInput.setText(title); } /** * Sets the search text. * <p/> * Note that this is the different from * {@link #setSearchBarTitle(CharSequence title) setSearchBarTitle} in * that it keeps the text when the search gains focus. * * @param text the text to be set for the search * input. */ public void setSearchText(CharSequence text) { mIsTitleSet = false; setQueryText(text); } /** * Returns the current query text. * * @return the current query */ public String getQuery() { return mOldQuery; } public void clearQuery() { mSearchInput.setText(""); } /** * Sets whether the search is focused or not. * * @param focused true, to set the search to be active/focused. * @return true if the search was focused and will now become not focused. Useful for * calling supper.onBackPress() in the hosting activity only if this method returns false */ public boolean setSearchFocused(final boolean focused) { boolean updatedToNotFocused = !focused && this.mIsFocused; if ((focused != this.mIsFocused) && mSuggestionSecHeightListener == null) { if (mIsSuggestionsSectionHeightSet) { setSearchFocusedInternal(focused); } else { mSuggestionSecHeightListener = new OnSuggestionSecHeightSetListener() { @Override public void onSuggestionSecHeightSet() { setSearchFocusedInternal(focused); mSuggestionSecHeightListener = null; } }; } } return updatedToNotFocused; } private void setupSuggestionSection() { LinearLayoutManager layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, true); mSuggestionsList.setLayoutManager(layoutManager); mSuggestionsList.setItemAnimator(null); final GestureDetector gestureDetector = new GestureDetector(getContext(), new GestureDetectorListenerAdapter() { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if (mHostActivity != null) { Util.closeSoftKeyboard(mHostActivity); } return false; } }); mSuggestionsList.addOnItemTouchListener(new OnItemTouchListenerAdapter() { @Override public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { gestureDetector.onTouchEvent(e); return false; } }); mSuggestionsAdapter = new SearchSuggestionsAdapter(getContext(), mSuggestionsTextSizePx, new SearchSuggestionsAdapter.Listener() { @Override public void onItemSelected(SearchSuggestion item) { mIsFocused = false; if (mSearchListener != null) { mSearchListener.onSuggestionClicked(item); } mSkipTextChangeEvent = true; if (mIsTitleSet) { setSearchBarTitle(item.getBody()); } else { setSearchText(item.getBody()); } setSearchFocusedInternal(false); } @Override public void onMoveItemToSearchClicked(SearchSuggestion item) { setQueryText(item.getBody()); } }); refreshShowMoveUpSuggestion(); mSuggestionsAdapter.setTextColor(this.mSuggestionTextColor); mSuggestionsAdapter.setRightIconColor(this.mSuggestionRightIconColor); mSuggestionsList.setAdapter(mSuggestionsAdapter); int cardViewBottomPadding = Util.dpToPx(CARD_VIEW_CORNERS_AND_TOP_BOTTOM_SHADOW_HEIGHT); //move up the suggestions section enough to cover the search bar //card's bottom left and right corners mSuggestionsSection.setTranslationY(-cardViewBottomPadding); } private void setQueryText(CharSequence text) { mSearchInput.setText(text); //move cursor to end of text mSearchInput.setSelection(mSearchInput.getText().length()); } private void moveSuggestListToInitialPos() { //move the suggestions list to the collapsed position //which is translationY of -listContainerHeight mSuggestionListContainer.setTranslationY(-mSuggestionListContainer.getHeight()); } /** * Clears the current suggestions and replaces it * with the provided list of new suggestions. * * @param newSearchSuggestions a list containing the new suggestions */ public void swapSuggestions(final List<? extends SearchSuggestion> newSearchSuggestions) { swapSuggestions(newSearchSuggestions, true); } private void swapSuggestions(final List<? extends SearchSuggestion> newSearchSuggestions, final boolean withAnim) { mSuggestionsList.getViewTreeObserver() .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { Util.removeGlobalLayoutObserver(mSuggestionsList, this); boolean isSuggestionItemsFillRecyclerView = updateSuggestionsSectionHeight( newSearchSuggestions, withAnim); //we only need to employ the reverse layout technique if the items don't fill up the RecyclerView LinearLayoutManager suggestionsListLm = (LinearLayoutManager) mSuggestionsList .getLayoutManager(); if (isSuggestionItemsFillRecyclerView) { suggestionsListLm.setReverseLayout(false); } else { Collections.reverse(newSearchSuggestions); mSuggestionsAdapter.notifyDataSetChanged(); suggestionsListLm.setReverseLayout(true); } } }); mSuggestionsList.setAdapter(mSuggestionsAdapter);//workaround to avoid list retaining scroll pos mSuggestionsAdapter.swapData(newSearchSuggestions); mDivider.setVisibility(!newSearchSuggestions.isEmpty() ? View.VISIBLE : View.GONE); } //returns true if the suggestion items occupy the full RecyclerView's height, false otherwise private boolean updateSuggestionsSectionHeight(List<? extends SearchSuggestion> newSearchSuggestions, boolean withAnim) { final int cardTopBottomShadowPadding = Util.dpToPx(CARD_VIEW_CORNERS_AND_TOP_BOTTOM_SHADOW_HEIGHT); final int cardRadiusSize = Util.dpToPx(CARD_VIEW_TOP_BOTTOM_SHADOW_HEIGHT); int visibleSuggestionHeight = calculateSuggestionItemsHeight(newSearchSuggestions, mSuggestionListContainer.getHeight()); int diff = mSuggestionListContainer.getHeight() - visibleSuggestionHeight; int addedTranslationYForShadowOffsets = (diff <= cardTopBottomShadowPadding) ? -(cardTopBottomShadowPadding - diff) : diff < (mSuggestionListContainer.getHeight() - cardTopBottomShadowPadding) ? cardRadiusSize : 0; final float newTranslationY = -mSuggestionListContainer.getHeight() + visibleSuggestionHeight + addedTranslationYForShadowOffsets; //todo go over final float fullyInvisibleTranslationY = -mSuggestionListContainer.getHeight() + cardRadiusSize; ViewCompat.animate(mSuggestionListContainer).cancel(); if (withAnim) { ViewCompat.animate(mSuggestionListContainer).setInterpolator(SUGGEST_ITEM_ADD_ANIM_INTERPOLATOR) .setDuration(mSuggestionSectionAnimDuration).translationY(newTranslationY) .setUpdateListener(new ViewPropertyAnimatorUpdateListener() { @Override public void onAnimationUpdate(View view) { if (mOnSuggestionsListHeightChanged != null) { float newSuggestionsHeight = Math .abs(view.getTranslationY() - fullyInvisibleTranslationY); mOnSuggestionsListHeightChanged .onSuggestionsListHeightChanged(newSuggestionsHeight); } } }).setListener(new ViewPropertyAnimatorListenerAdapter() { @Override public void onAnimationCancel(View view) { mSuggestionListContainer.setTranslationY(newTranslationY); } }).start(); } else { mSuggestionListContainer.setTranslationY(newTranslationY); if (mOnSuggestionsListHeightChanged != null) { float newSuggestionsHeight = Math .abs(mSuggestionListContainer.getTranslationY() - fullyInvisibleTranslationY); mOnSuggestionsListHeightChanged.onSuggestionsListHeightChanged(newSuggestionsHeight); } } return mSuggestionListContainer.getHeight() == visibleSuggestionHeight; } //returns the cumulative height that the current suggestion items take up or the given max if the //results is >= max. The max option allows us to avoid doing unnecessary and potentially long calculations. private int calculateSuggestionItemsHeight(List<? extends SearchSuggestion> suggestions, int max) { //todo // 'i < suggestions.size()' in the below 'for' seems unneeded, investigate if there is a use for it. int visibleItemsHeight = 0; for (int i = 0; i < suggestions.size() && i < mSuggestionsList.getChildCount(); i++) { visibleItemsHeight += mSuggestionsList.getChildAt(i).getHeight(); if (visibleItemsHeight > max) { visibleItemsHeight = max; break; } } return visibleItemsHeight; } /** * Set a callback that will be called after each suggestion view in the suggestions recycler * list is bound. This allows for customized binding for specific items in the list. * * @param callback A callback to be called after a suggestion is bound by the suggestions list's * adapter. */ public void setOnBindSuggestionCallback(SearchSuggestionsAdapter.OnBindSuggestionCallback callback) { this.mOnBindSuggestionCallback = callback; if (mSuggestionsAdapter != null) { mSuggestionsAdapter.setOnBindSuggestionCallback(mOnBindSuggestionCallback); } } /** * Collapses the suggestions list and * then clears its suggestion items. */ public void clearSuggestions() { swapSuggestions(new ArrayList<SearchSuggestion>()); } public void clearSearchFocus() { setSearchFocusedInternal(false); } public boolean isSearchBarFocused() { return mIsFocused; } private void setSearchFocusedInternal(final boolean focused) { this.mIsFocused = focused; if (focused) { mSearchInput.requestFocus(); moveSuggestListToInitialPos(); mSuggestionsSection.setVisibility(VISIBLE); if (mDimBackground) { fadeInBackground(); } handleOnVisibleMenuItemsWidthChanged(0);//this must be called before mMenuView.hideIfRoomItems(...) mMenuView.hideIfRoomItems(true); transitionInLeftSection(true); Util.showSoftKeyboard(getContext(), mSearchInput); if (mMenuOpen) { closeMenu(false); } if (mIsTitleSet) { mSkipTextChangeEvent = true; mSearchInput.setText(""); } else { mSearchInput.setSelection(mSearchInput.getText().length()); } mSearchInput.setLongClickable(true); mClearButton.setVisibility( (mSearchInput.getText().toString().length() == 0) ? View.INVISIBLE : View.VISIBLE); if (mFocusChangeListener != null) { mFocusChangeListener.onFocus(); } } else { mMainLayout.requestFocus(); clearSuggestions(); if (mDimBackground) { fadeOutBackground(); } handleOnVisibleMenuItemsWidthChanged(0);//this must be called before mMenuView.hideIfRoomItems(...) mMenuView.showIfRoomItems(true); transitionOutLeftSection(true); mClearButton.setVisibility(View.GONE); if (mHostActivity != null) { Util.closeSoftKeyboard(mHostActivity); } if (mIsTitleSet) { mSkipTextChangeEvent = true; mSearchInput.setText(mTitleText); } mSearchInput.setLongClickable(false); if (mFocusChangeListener != null) { mFocusChangeListener.onFocusCleared(); } } //if we don't have focus, we want to allow the client's views below our invisible //screen-covering view to handle touches mSuggestionsSection.setEnabled(focused); } private void changeIcon(ImageView imageView, Drawable newIcon, boolean withAnim) { imageView.setImageDrawable(newIcon); if (withAnim) { ObjectAnimator fadeInVoiceInputOrClear = ObjectAnimator.ofFloat(imageView, "alpha", 0.0f, 1.0f); fadeInVoiceInputOrClear.start(); } else { imageView.setAlpha(1.0f); } } private void transitionInLeftSection(boolean withAnim) { if (mSearchProgress.getVisibility() != View.VISIBLE) { mLeftAction.setVisibility(View.VISIBLE); } else { mLeftAction.setVisibility(View.INVISIBLE); } switch (mLeftActionMode) { case LEFT_ACTION_MODE_SHOW_HAMBURGER: openMenuDrawable(mMenuBtnDrawable, withAnim); if (!mMenuOpen) { break; } break; case LEFT_ACTION_MODE_SHOW_SEARCH: mLeftAction.setImageDrawable(mIconBackArrow); if (withAnim) { mLeftAction.setRotation(45); mLeftAction.setAlpha(0.0f); ObjectAnimator rotateAnim = ViewPropertyObjectAnimator.animate(mLeftAction).rotation(0).get(); ObjectAnimator fadeAnim = ViewPropertyObjectAnimator.animate(mLeftAction).alpha(1.0f).get(); AnimatorSet animSet = new AnimatorSet(); animSet.setDuration(500); animSet.playTogether(rotateAnim, fadeAnim); animSet.start(); } break; case LEFT_ACTION_MODE_SHOW_HOME: //do nothing break; case LEFT_ACTION_MODE_NO_LEFT_ACTION: mLeftAction.setImageDrawable(mIconBackArrow); if (withAnim) { ObjectAnimator searchInputTransXAnim = ViewPropertyObjectAnimator.animate(mSearchInputParent) .translationX(0).get(); mLeftAction.setScaleX(0.5f); mLeftAction.setScaleY(0.5f); mLeftAction.setAlpha(0.0f); mLeftAction.setTranslationX(Util.dpToPx(8)); ObjectAnimator transXArrowAnim = ViewPropertyObjectAnimator.animate(mLeftAction).translationX(1.0f) .get(); ObjectAnimator scaleXArrowAnim = ViewPropertyObjectAnimator.animate(mLeftAction).scaleX(1.0f).get(); ObjectAnimator scaleYArrowAnim = ViewPropertyObjectAnimator.animate(mLeftAction).scaleY(1.0f).get(); ObjectAnimator fadeArrowAnim = ViewPropertyObjectAnimator.animate(mLeftAction).alpha(1.0f).get(); transXArrowAnim.setStartDelay(150); scaleXArrowAnim.setStartDelay(150); scaleYArrowAnim.setStartDelay(150); fadeArrowAnim.setStartDelay(150); AnimatorSet animSet = new AnimatorSet(); animSet.setDuration(500); animSet.playTogether(searchInputTransXAnim, transXArrowAnim, scaleXArrowAnim, scaleYArrowAnim, fadeArrowAnim); animSet.start(); } else { mSearchInputParent.setTranslationX(0); } break; } } private void transitionOutLeftSection(boolean withAnim) { switch (mLeftActionMode) { case LEFT_ACTION_MODE_SHOW_HAMBURGER: closeMenuDrawable(mMenuBtnDrawable, withAnim); break; case LEFT_ACTION_MODE_SHOW_SEARCH: changeIcon(mLeftAction, mIconSearch, withAnim); break; case LEFT_ACTION_MODE_SHOW_HOME: //do nothing break; case LEFT_ACTION_MODE_NO_LEFT_ACTION: mLeftAction.setImageDrawable(mIconBackArrow); if (withAnim) { ObjectAnimator searchInputTransXAnim = ViewPropertyObjectAnimator.animate(mSearchInputParent) .translationX(-Util.dpToPx(LEFT_MENU_WIDTH_AND_MARGIN_START_DP)).get(); ObjectAnimator scaleXArrowAnim = ViewPropertyObjectAnimator.animate(mLeftAction).scaleX(0.5f).get(); ObjectAnimator scaleYArrowAnim = ViewPropertyObjectAnimator.animate(mLeftAction).scaleY(0.5f).get(); ObjectAnimator fadeArrowAnim = ViewPropertyObjectAnimator.animate(mLeftAction).alpha(0.5f).get(); scaleXArrowAnim.setDuration(300); scaleYArrowAnim.setDuration(300); fadeArrowAnim.setDuration(300); scaleXArrowAnim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { //restore normal state mLeftAction.setScaleX(1.0f); mLeftAction.setScaleY(1.0f); mLeftAction.setAlpha(1.0f); mLeftAction.setVisibility(View.INVISIBLE); } }); AnimatorSet animSet = new AnimatorSet(); animSet.setDuration(350); animSet.playTogether(scaleXArrowAnim, scaleYArrowAnim, fadeArrowAnim, searchInputTransXAnim); animSet.start(); } else { mLeftAction.setVisibility(View.INVISIBLE); } break; } } /** * Sets the listener that will be notified when the suggestion list's height * changes. * * @param onSuggestionsListHeightChanged the new suggestions list's height */ public void setOnSuggestionsListHeightChanged(OnSuggestionsListHeightChanged onSuggestionsListHeightChanged) { this.mOnSuggestionsListHeightChanged = onSuggestionsListHeightChanged; } /** * Sets the listener that will listen for query * changes as they are being typed. * * @param listener listener for query changes */ public void setOnQueryChangeListener(OnQueryChangeListener listener) { this.mQueryListener = listener; } /** * Sets the listener that will be called when * an action that completes the current search * session has occurred and the search lost focus. * <p/> * <p>When called, a client would ideally grab the * search or suggestion query from the callback parameter or * from {@link #getQuery() getquery} and perform the necessary * query against its data source.</p> * * @param listener listener for query completion */ public void setOnSearchListener(OnSearchListener listener) { this.mSearchListener = listener; } /** * Sets the listener that will be called when the focus * of the search has changed. * * @param listener listener for search focus changes */ public void setOnFocusChangeListener(OnFocusChangeListener listener) { this.mFocusChangeListener = listener; } /** * Sets the listener that will be called when the * left/start menu (or navigation menu) is clicked. * <p/> * <p>Note that this is different from the overflow menu * that has a separate listener.</p> * * @param listener */ public void setOnLeftMenuClickListener(OnLeftMenuClickListener listener) { this.mOnMenuClickListener = listener; } /** * Sets the listener that will be called when the * left/start home action (back arrow) is clicked. * * @param listener */ public void setOnHomeActionClickListener(OnHomeActionClickListener listener) { this.mOnHomeActionClickListener = listener; } /** * Sets the listener that will be called when * an item in the overflow menu is clicked. * * @param listener listener to listen to menu item clicks */ public void setOnMenuItemClickListener(OnMenuItemClickListener listener) { this.mActionMenuItemListener = listener; //todo reset menu view listener } /** * Sets the listener that will be called when the * clear search text action button (the x to the right * of the search text) is clicked. * * @param listener */ public void setOnClearSearchActionListener(OnClearSearchActionListener listener) { this.mOnClearSearchActionListener = listener; } private void openMenuDrawable(final DrawerArrowDrawable drawerArrowDrawable, boolean withAnim) { if (withAnim) { ValueAnimator anim = ValueAnimator.ofFloat(0.0f, 1.0f); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float value = (Float) animation.getAnimatedValue(); drawerArrowDrawable.setProgress(value); } }); anim.setDuration(MENU_ICON_ANIM_DURATION); anim.start(); } else { drawerArrowDrawable.setProgress(1.0f); } } private void closeMenuDrawable(final DrawerArrowDrawable drawerArrowDrawable, boolean withAnim) { if (withAnim) { ValueAnimator anim = ValueAnimator.ofFloat(1.0f, 0.0f); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float value = (Float) animation.getAnimatedValue(); drawerArrowDrawable.setProgress(value); } }); anim.setDuration(MENU_ICON_ANIM_DURATION); anim.start(); } else { drawerArrowDrawable.setProgress(0.0f); } } private void fadeOutBackground() { ValueAnimator anim = ValueAnimator.ofInt(BACKGROUND_DRAWABLE_ALPHA_SEARCH_FOCUSED, BACKGROUND_DRAWABLE_ALPHA_SEARCH_NOT_FOCUSED); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int value = (Integer) animation.getAnimatedValue(); mBackgroundDrawable.setAlpha(value); } }); anim.setDuration(BACKGROUND_FADE_ANIM_DURATION); anim.start(); } private void fadeInBackground() { ValueAnimator anim = ValueAnimator.ofInt(BACKGROUND_DRAWABLE_ALPHA_SEARCH_NOT_FOCUSED, BACKGROUND_DRAWABLE_ALPHA_SEARCH_FOCUSED); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int value = (Integer) animation.getAnimatedValue(); mBackgroundDrawable.setAlpha(value); } }); anim.setDuration(BACKGROUND_FADE_ANIM_DURATION); anim.start(); } private boolean isRTL() { Configuration config = getResources().getConfiguration(); return ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL; } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState savedState = new SavedState(superState); savedState.suggestions = this.mSuggestionsAdapter.getDataSet(); savedState.isFocused = this.mIsFocused; savedState.query = getQuery(); savedState.suggestionTextSize = this.mSuggestionsTextSizePx; savedState.searchHint = this.mSearchHint; savedState.dismissOnOutsideClick = this.mDismissOnOutsideTouch; savedState.showMoveSuggestionUpBtn = this.mShowMoveUpSuggestion; savedState.showSearchKey = this.mShowSearchKey; savedState.isTitleSet = this.mIsTitleSet; savedState.backgroundColor = this.mBackgroundColor; savedState.suggestionsTextColor = this.mSuggestionTextColor; savedState.queryTextColor = this.mSearchInputTextColor; savedState.searchHintTextColor = this.mSearchInputHintColor; savedState.actionOverflowMenueColor = this.mOverflowIconColor; savedState.menuItemIconColor = this.mActionMenuItemColor; savedState.leftIconColor = this.mLeftActionIconColor; savedState.clearBtnColor = this.mClearBtnColor; savedState.suggestionUpBtnColor = this.mSuggestionTextColor; savedState.dividerColor = this.mDividerColor; savedState.menuId = mMenuId; savedState.leftActionMode = mLeftActionMode; savedState.dimBackground = mDimBackground; savedState.dismissOnSoftKeyboardDismiss = this.mDismissOnOutsideTouch; return savedState; } @Override public void onRestoreInstanceState(Parcelable state) { final SavedState savedState = (SavedState) state; super.onRestoreInstanceState(savedState.getSuperState()); this.mIsFocused = savedState.isFocused; this.mIsTitleSet = savedState.isTitleSet; this.mMenuId = savedState.menuId; this.mSuggestionSectionAnimDuration = savedState.suggestionsSectionAnimSuration; setSuggestionItemTextSize(savedState.suggestionTextSize); setDismissOnOutsideClick(savedState.dismissOnOutsideClick); setShowMoveUpSuggestion(savedState.showMoveSuggestionUpBtn); setShowSearchKey(savedState.showSearchKey); setSearchHint(savedState.searchHint); setBackgroundColor(savedState.backgroundColor); setSuggestionsTextColor(savedState.suggestionsTextColor); setQueryTextColor(savedState.queryTextColor); setQueryTextSize(savedState.queryTextSize); setHintTextColor(savedState.searchHintTextColor); setActionMenuOverflowColor(savedState.actionOverflowMenueColor); setMenuItemIconColor(savedState.menuItemIconColor); setLeftActionIconColor(savedState.leftIconColor); setClearBtnColor(savedState.clearBtnColor); setSuggestionRightIconColor(savedState.suggestionUpBtnColor); setDividerColor(savedState.dividerColor); setLeftActionMode(savedState.leftActionMode); setDimBackground(savedState.dimBackground); setCloseSearchOnKeyboardDismiss(savedState.dismissOnSoftKeyboardDismiss); mSuggestionsSection.setEnabled(this.mIsFocused); if (this.mIsFocused) { mBackgroundDrawable.setAlpha(BACKGROUND_DRAWABLE_ALPHA_SEARCH_FOCUSED); mSkipTextChangeEvent = true; mSkipQueryFocusChangeEvent = true; mSuggestionsSection.setVisibility(VISIBLE); //restore suggestions list when suggestion section's height is fully set mSuggestionSecHeightListener = new OnSuggestionSecHeightSetListener() { @Override public void onSuggestionSecHeightSet() { swapSuggestions(savedState.suggestions, false); mSuggestionSecHeightListener = null; //todo refactor move to a better location transitionInLeftSection(false); } }; mClearButton.setVisibility((savedState.query.length() == 0) ? View.INVISIBLE : View.VISIBLE); mLeftAction.setVisibility(View.VISIBLE); Util.showSoftKeyboard(getContext(), mSearchInput); } } private DrawerLayout.DrawerListener mDrawerListener = new DrawerListener(); public void attachNavigationDrawerToMenuButton(@NonNull DrawerLayout drawerLayout) { drawerLayout.addDrawerListener(mDrawerListener); setOnLeftMenuClickListener(new NavDrawerLeftMenuClickListener(drawerLayout)); } public void detachNavigationDrawerFromMenuButton(@NonNull DrawerLayout drawerLayout) { drawerLayout.removeDrawerListener(mDrawerListener); setOnLeftMenuClickListener(null); } static class SavedState extends BaseSavedState { private List<? extends SearchSuggestion> suggestions = new ArrayList<>(); private boolean isFocused; private String query; private int queryTextSize; private int suggestionTextSize; private String searchHint; private boolean dismissOnOutsideClick; private boolean showMoveSuggestionUpBtn; private boolean showSearchKey; private boolean isTitleSet; private int backgroundColor; private int suggestionsTextColor; private int queryTextColor; private int searchHintTextColor; private int actionOverflowMenueColor; private int menuItemIconColor; private int leftIconColor; private int clearBtnColor; private int suggestionUpBtnColor; private int dividerColor; private int menuId; private int leftActionMode; private boolean dimBackground; private long suggestionsSectionAnimSuration; private boolean dismissOnSoftKeyboardDismiss; SavedState(Parcelable superState) { super(superState); } private SavedState(Parcel in) { super(in); in.readList(suggestions, getClass().getClassLoader()); isFocused = (in.readInt() != 0); query = in.readString(); queryTextSize = in.readInt(); suggestionTextSize = in.readInt(); searchHint = in.readString(); dismissOnOutsideClick = (in.readInt() != 0); showMoveSuggestionUpBtn = (in.readInt() != 0); showSearchKey = (in.readInt() != 0); isTitleSet = (in.readInt() != 0); backgroundColor = in.readInt(); suggestionsTextColor = in.readInt(); queryTextColor = in.readInt(); searchHintTextColor = in.readInt(); actionOverflowMenueColor = in.readInt(); menuItemIconColor = in.readInt(); leftIconColor = in.readInt(); clearBtnColor = in.readInt(); suggestionUpBtnColor = in.readInt(); dividerColor = in.readInt(); menuId = in.readInt(); leftActionMode = in.readInt(); dimBackground = (in.readInt() != 0); suggestionsSectionAnimSuration = in.readLong(); dismissOnSoftKeyboardDismiss = (in.readInt() != 0); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeList(suggestions); out.writeInt(isFocused ? 1 : 0); out.writeString(query); out.writeInt(queryTextSize); out.writeInt(suggestionTextSize); out.writeString(searchHint); out.writeInt(dismissOnOutsideClick ? 1 : 0); out.writeInt(showMoveSuggestionUpBtn ? 1 : 0); out.writeInt(showSearchKey ? 1 : 0); out.writeInt(isTitleSet ? 1 : 0); out.writeInt(backgroundColor); out.writeInt(suggestionsTextColor); out.writeInt(queryTextColor); out.writeInt(searchHintTextColor); out.writeInt(actionOverflowMenueColor); out.writeInt(menuItemIconColor); out.writeInt(leftIconColor); out.writeInt(clearBtnColor); out.writeInt(suggestionUpBtnColor); out.writeInt(dividerColor); out.writeInt(menuId); out.writeInt(leftActionMode); out.writeInt(dimBackground ? 1 : 0); out.writeLong(suggestionsSectionAnimSuration); out.writeInt(dismissOnSoftKeyboardDismiss ? 1 : 0); } public static final Creator<SavedState> CREATOR = new Creator<SavedState>() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); //remove any ongoing animations to prevent leaks //todo investigate if correct ViewCompat.animate(mSuggestionListContainer).cancel(); } private class DrawerListener implements DrawerLayout.DrawerListener { @Override public void onDrawerSlide(View drawerView, float slideOffset) { setMenuIconProgress(slideOffset); } @Override public void onDrawerOpened(View drawerView) { } @Override public void onDrawerClosed(View drawerView) { } @Override public void onDrawerStateChanged(int newState) { } } private class NavDrawerLeftMenuClickListener implements OnLeftMenuClickListener { DrawerLayout mDrawerLayout; public NavDrawerLeftMenuClickListener(DrawerLayout drawerLayout) { mDrawerLayout = drawerLayout; } @Override public void onMenuOpened() { mDrawerLayout.openDrawer(GravityCompat.START); } @Override public void onMenuClosed() { //do nothing } } }