Last active
February 19, 2020 12:12
-
-
Save sbelloz/5193779a4c0db216c7e491113005aa36 to your computer and use it in GitHub Desktop.
Suggestion search view collapsable
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<resources> | |
<declare-styleable name="SuggestionSearchView"> | |
<attr name="ssv_enableBack" format="boolean"/> | |
<attr name="ssv_searchHint" format="string"/> | |
<attr name="ssv_searchTextColor" format="color"/> | |
<attr name="ssv_searchTextHintColor" format="color"/> | |
<attr name="ssv_searchOptionsIcon" format="reference"/> | |
<attr name="ssv_clearTextOnDismiss" format="boolean"/> | |
<attr name="ssv_waitKeyboardForAnimation" format="boolean"/> | |
<attr name="ssv_delayAnimationIn" format="integer"/> | |
</declare-styleable> | |
</resources> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<resources xmlns:tools="http://schemas.android.com/tools"> | |
<color name="search_bar_bg">#cccccc</color> | |
</resources> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<resources xmlns:tools="http://schemas.android.com/tools"> | |
<dimen name="suggestion_view_height">54dp</dimen> | |
<dimen name="dimen4">4dp</dimen> | |
<dimen name="dimen8">8dp</dimen> | |
<dimen name="dimen10">10dp</dimen> | |
<dimen name="dimen12">12dp</dimen> | |
<dimen name="dimen16">16dp</dimen> | |
</resources> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:app="http://schemas.android.com/apk/res-auto" | |
xmlns:tools="http://schemas.android.com/tools" | |
android:id="@+id/search_suggestions_container" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:background="@color/transparent"> | |
<androidx.cardview.widget.CardView | |
android:id="@+id/suggestions_list_container" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:visibility="invisible" | |
app:cardUseCompatPadding="true"> | |
<androidx.recyclerview.widget.RecyclerView | |
android:id="@+id/suggestions_list" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:clipToPadding="false" | |
android:isScrollContainer="false" | |
tools:visibility="visible" /> | |
<!--android:overScrollMode="never"--> | |
</androidx.cardview.widget.CardView> | |
</FrameLayout> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<merge xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:app="http://schemas.android.com/apk/res-auto" | |
xmlns:tools="http://schemas.android.com/tools" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
tools:orientation="vertical" | |
tools:parentTag="android.widget.LinearLayout"> | |
<FrameLayout | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content"> | |
<FrameLayout | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content"> | |
<androidx.cardview.widget.CardView | |
android:id="@+id/search_bar_view" | |
android:layout_width="match_parent" | |
android:layout_height="@dimen/suggestion_view_height" | |
app:cardUseCompatPadding="true"> | |
<RelativeLayout | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:layout_gravity="center_vertical"> | |
<ImageView | |
android:id="@+id/btn_back" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:layout_centerVertical="true" | |
android:layout_marginTop="@dimen/dimen8" | |
android:layout_marginStart="@dimen/dimen8" | |
android:layout_marginBottom="@dimen/dimen8" | |
android:visibility="gone" | |
android:background="?attr/selectableItemBackgroundBorderless" | |
android:src="@drawable/ic_action_arrow_back" | |
tools:ignore="ContentDescription" /> | |
<it.immobiliare.android.widget.SearchInputView | |
android:id="@+id/search_edit_view" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:layout_alignWithParentIfMissing="true" | |
android:layout_centerVertical="true" | |
android:layout_toEndOf="@id/btn_back" | |
android:layout_toStartOf="@+id/btn_clear" | |
android:layout_marginStart="@dimen/dimen8" | |
android:background="@color/transparent" | |
android:imeOptions="flagNoFullscreen|flagNoExtractUi|actionSearch" | |
android:inputType="textNoSuggestions" | |
android:textColor="@color/grey_xlight" | |
android:textSize="@dimen/text_medium" | |
android:ellipsize="end" | |
tools:text="Search..."/> | |
<ImageView | |
android:id="@+id/btn_clear" | |
android:layout_width="32dp" | |
android:layout_height="32dp" | |
android:layout_toStartOf="@+id/btn_options_separator_view" | |
android:layout_alignWithParentIfMissing="true" | |
android:layout_centerVertical="true" | |
android:layout_margin="@dimen/dimen8" | |
android:background="?attr/selectableItemBackgroundBorderless" | |
android:padding="@dimen/dimen8" | |
android:visibility="gone" | |
app:srcCompat="@drawable/ic_erase" | |
tools:ignore="ContentDescription" | |
tools:visibility="visible" /> | |
<ImageView | |
android:id="@+id/btn_options_separator_view" | |
android:layout_width="1dp" | |
android:layout_height="match_parent" | |
android:layout_alignParentBottom="true" | |
android:layout_alignParentTop="@+id/btn_options" | |
android:layout_toStartOf="@id/btn_options" | |
android:background="@color/search_bar_bg" /> | |
<ImageView | |
android:id="@+id/btn_options" | |
android:layout_width="@dimen/dimen_32" | |
android:layout_height="@dimen/dimen_32" | |
android:layout_alignParentEnd="true" | |
android:layout_centerVertical="true" | |
android:background="?attr/selectableItemBackgroundBorderless" | |
android:layout_margin="@dimen/dimen8" | |
tools:ignore="ContentDescription" | |
tools:srcCompat="@drawable/ic_posizione" /> | |
</RelativeLayout> | |
</androidx.cardview.widget.CardView> | |
</FrameLayout> | |
<View | |
android:id="@+id/divider_view" | |
android:layout_width="match_parent" | |
android:layout_height="1dp" | |
android:layout_gravity="bottom" | |
android:layout_marginBottom="5dp" | |
android:layout_marginStart="2dp" | |
android:layout_marginEnd="2dp" | |
android:background="@color/search_bar_bg" | |
android:visibility="gone" | |
tools:visibility="visible" /> | |
</FrameLayout> | |
<include layout="@layout/suggestion_list_view" /> | |
</merge> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @author Simone Bellotti | |
*/ | |
public class SuggestionSearchView extends LinearLayout { | |
private static final String TAG = "SuggestionSearchView"; | |
private static final double COS_45 = Math.cos(Math.toRadians(45)); | |
private static final boolean DEFAULT_ENABLED_BACK = true; | |
private static final boolean DEFAULT_WAIT_KEYBOARD_FOR_COLLAPSE = true; | |
private static final boolean DEFAULT_CLEAR_TEXT_ON_DISMISS = true; | |
private static final int DEFAULT_DELAY_ANIMATION_IN = -1; | |
private static final FastOutSlowInInterpolator ANIM_INTERPOLATOR = | |
new FastOutSlowInInterpolator(); | |
private static final int ANIM_DURATION = 200; | |
//The CardView's top or bottom height used for its shadow | |
private static final int CARD_VIEW_TOP_BOTTOM_SHADOW_HEIGHT = 3; | |
//The CardView's (default) corner radius height | |
private static final int CARD_VIEW_CORNERS_HEIGHT = 2; | |
private static final int CARD_VIEW_CORNERS_AND_TOP_BOTTOM_SHADOW_HEIGHT = | |
CARD_VIEW_TOP_BOTTOM_SHADOW_HEIGHT + CARD_VIEW_CORNERS_HEIGHT; | |
private boolean isAdjustPanMode; | |
public interface OnFocusChangeListener { | |
void onFocus(); | |
void onFocusCleared(); | |
} | |
public interface OnQueryTextChangeListener { | |
void onQueryChanged(CharSequence text); | |
} | |
public interface OnSuggestionsAnimationListener { | |
void onSuggestionsIn(); | |
void onSuggestionsOut(); | |
} | |
public abstract static class SuggestionsAnimationAdapter | |
implements OnSuggestionsAnimationListener { | |
@Override | |
public void onSuggestionsIn() { | |
} | |
@Override | |
public void onSuggestionsOut() { | |
} | |
} | |
@BindView(R2.id.btn_back) | |
ImageView btnBack; | |
@BindView(R2.id.search_edit_view) | |
SearchInputView searchView; | |
@BindView(R2.id.suggestions_list) | |
RecyclerView suggestionsList; | |
@BindView(R2.id.search_suggestions_container) | |
FrameLayout suggestionsContainer; | |
@BindView(R2.id.suggestions_list_container) | |
CardView suggestionListContainer; | |
@BindView(R2.id.search_bar_view) | |
CardView searchBarView; | |
@BindView(R2.id.btn_clear) | |
ImageView btnClear; | |
@BindView(R2.id.divider_view) | |
View dividerView; | |
@BindView(R2.id.btn_options_separator_view) | |
ImageView separatorView; | |
@BindView(R2.id.btn_options) | |
ImageView btnOptions; | |
@BindDimen(R2.dimen.dimen16) | |
int dimen16; | |
private TextWatcher textWatcher; | |
KeyListener keyListener; | |
ValueAnimator valueAnimator; | |
View activityRootView; | |
@Nullable | |
OnFocusChangeListener onFocusChangeListener; | |
@Nullable | |
TextView.OnEditorActionListener onEditorActionListener; | |
OnClickListener onBackClickListener; | |
OnClickListener onBtnClearClickListener; | |
OnQueryTextChangeListener onQueryTextChangeListener; | |
OnSuggestionsAnimationListener onListAnimationListener; | |
RecyclerView.AdapterDataObserver adapterObserver; | |
boolean isAnimating; | |
boolean isFocused; | |
boolean isOptionsIconEnabled; | |
boolean isInitialLayout = true; | |
boolean skipTextChangeEvent; | |
boolean waitKeyboardForCollapse = DEFAULT_WAIT_KEYBOARD_FOR_COLLAPSE; | |
boolean clearTextOnDismiss = DEFAULT_CLEAR_TEXT_ON_DISMISS; | |
int delayAnimationIn = DEFAULT_DELAY_ANIMATION_IN; | |
@Px | |
int endListHeight; | |
@Px | |
int startListHeight; | |
@Px | |
int bottomInsets; | |
public SuggestionSearchView(Context context) { | |
super(context); | |
init(null); | |
} | |
public SuggestionSearchView(Context context, @Nullable AttributeSet attrs) { | |
super(context, attrs); | |
init(attrs); | |
} | |
public SuggestionSearchView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { | |
super(context, attrs, defStyleAttr); | |
init(attrs); | |
} | |
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) | |
public SuggestionSearchView( | |
Context context, | |
AttributeSet attrs, int defStyleAttr, | |
int defStyleRes | |
) { | |
super(context, attrs, defStyleAttr, defStyleRes); | |
init(attrs); | |
} | |
private void init(@Nullable AttributeSet attrs) { | |
inflate(getContext(), R.layout.suggestion_search_layout, this); | |
ButterKnife.bind(this); | |
setOrientation(VERTICAL); | |
setClipToPadding(false); | |
setBackgroundColor(ContextCompat.getColor(getContext(), R.color.transparent)); | |
setFocusable(true); | |
setFocusableInTouchMode(true); | |
if (attrs != null) { | |
applyXmlAttributes(attrs); | |
} | |
initSearchInputView(); | |
initSuggestionsList(); | |
initClearBtn(); | |
initSuggestionsContainer(); | |
initSuggestionsListContainer(); | |
initDividerView(); | |
isAdjustPanMode = (((AppCompatActivity) getContext()).getWindow() | |
.getAttributes().softInputMode & WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) != 0; | |
adapterObserver = new RecyclerView.AdapterDataObserver() { | |
@Override | |
public void onChanged() { | |
if (isFocused) { | |
toggleDividerView(getItemCount() > 0); | |
} | |
} | |
}; | |
} | |
@RequiresApi(api = Build.VERSION_CODES.KITKAT_WATCH) | |
@Override | |
public WindowInsets onApplyWindowInsets(WindowInsets insets) { | |
bottomInsets = insets.getSystemWindowInsetBottom(); | |
return insets.consumeSystemWindowInsets(); | |
} | |
@Override | |
protected void onDetachedFromWindow() { | |
super.onDetachedFromWindow(); | |
onQueryTextChangeListener = null; | |
onFocusChangeListener = null; | |
onListAnimationListener = null; | |
removePendingAnimation(); | |
removePendingDataObserver(); | |
} | |
private void initDividerView() { | |
int padding = (int) Math.ceil(calculateHorizontalPadding(suggestionListContainer)); | |
MarginLayoutParams layoutParams = (MarginLayoutParams) dividerView.getLayoutParams(); | |
layoutParams.setMarginStart(padding); | |
layoutParams.setMarginEnd(padding); | |
} | |
private void applyXmlAttributes(AttributeSet attrs) { | |
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.SuggestionSearchView); | |
try { | |
initBackButton( | |
a.getBoolean(R.styleable.SuggestionSearchView_ssv_enableBack, DEFAULT_ENABLED_BACK) | |
); | |
setHint(a.getString(R.styleable.SuggestionSearchView_ssv_searchHint)); | |
setTextHintColor( | |
a.getColor(R.styleable.SuggestionSearchView_ssv_searchTextHintColor, Color.LTGRAY) | |
); | |
setSearchTextColor( | |
a.getColor(R.styleable.SuggestionSearchView_ssv_searchTextColor, Color.BLACK) | |
); | |
waitKeyboardForCollapse = a.getBoolean( | |
R.styleable.SuggestionSearchView_ssv_waitKeyboardForAnimation, | |
DEFAULT_WAIT_KEYBOARD_FOR_COLLAPSE | |
); | |
clearTextOnDismiss = a.getBoolean( | |
R.styleable.SuggestionSearchView_ssv_clearTextOnDismiss, | |
DEFAULT_CLEAR_TEXT_ON_DISMISS | |
); | |
delayAnimationIn = a.getInt( | |
R.styleable.SuggestionSearchView_ssv_delayAnimationIn, | |
DEFAULT_DELAY_ANIMATION_IN | |
); | |
int optionIconResId = | |
a.getResourceId(R.styleable.SuggestionSearchView_ssv_searchOptionsIcon, 0); | |
isOptionsIconEnabled = optionIconResId > 0; | |
if (isOptionsIconEnabled) { | |
setOptionBtnIcon(optionIconResId); | |
} | |
toggleOptionsButtonVisibility(isOptionsIconEnabled); | |
} finally { | |
a.recycle(); | |
} | |
} | |
public boolean areSuggestionsVisible() { | |
return suggestionListContainer.getVisibility() == VISIBLE; | |
} | |
public CharSequence getText() { | |
return searchView.getText(); | |
} | |
public void setOptionBtnIcon(@DrawableRes int resId) { | |
btnOptions.setImageResource(resId); | |
} | |
public void setOnOptionBtnClickListener(OnClickListener onClickListener) { | |
btnOptions.setOnClickListener(onClickListener); | |
} | |
public void setOnButtonClearClickListener(OnClickListener onBtnClearClickListener) { | |
this.onBtnClearClickListener = onBtnClearClickListener; | |
} | |
@Override | |
public boolean isFocused() { | |
return isFocused; | |
} | |
public int getSearchBarHeight() { | |
return searchBarView.getHeight(); | |
} | |
@Override | |
public void onWindowFocusChanged(boolean hasWindowFocus) { | |
super.onWindowFocusChanged(hasWindowFocus); | |
if (!hasWindowFocus && isFocused) { | |
setFocusInternally(false); | |
} | |
} | |
public void setTextHintColor(@ColorInt int color) { | |
searchView.setHintTextColor(color); | |
} | |
public void setSearchTextColor(@ColorInt int color) { | |
searchView.setTextColor(color); | |
} | |
public void setHint(@Nullable String searchHint) { | |
String hintText = searchHint != null | |
? searchHint | |
: getResources().getString(R.string.abc_search_hint); | |
searchView.setHint(hintText); | |
} | |
public void setHint(@StringRes int resId) { | |
searchView.setHint(resId); | |
} | |
public void setText(CharSequence text) { | |
searchView.setText(text); | |
searchView.setSelection(searchView.getText().length()); | |
} | |
public void setTextIgnoringWatcher(@Nullable CharSequence text) { | |
searchView.setText(null); | |
searchView.removeTextChangedListener(textWatcher); | |
if (!TextUtils.isEmpty(text)) { | |
searchView.setText(text); | |
searchView.setSelection(searchView.getText().length()); | |
toggleCancelButtonVisibility(text.length() > 0); | |
} | |
searchView.addTextChangedListener(textWatcher); | |
} | |
@Override | |
public void setEnabled(boolean enabled) { | |
super.setEnabled(enabled); | |
searchView.setEnabled(enabled); | |
} | |
public void setWaitKeyboardForCollapse(boolean waitKeyboardForCollapse) { | |
this.waitKeyboardForCollapse = waitKeyboardForCollapse; | |
} | |
public void setAdapter(final RecyclerView.Adapter adapter) { | |
suggestionsList.setAdapter(adapter); | |
if (adapter != null) { | |
adapter.registerAdapterDataObserver(adapterObserver); | |
} | |
} | |
public void toggleDividerView(boolean enabled) { | |
dividerView.setVisibility(enabled ? VISIBLE : GONE); | |
} | |
public void setOnFocusChangeListener(@Nullable OnFocusChangeListener onFocusChangeListener) { | |
this.onFocusChangeListener = onFocusChangeListener; | |
} | |
public void setOnBackButtonClickListener(OnClickListener onClickListener) { | |
this.onBackClickListener = onClickListener; | |
} | |
public void setOnQueryTextChangeListener(OnQueryTextChangeListener onQueryTextChangeListener) { | |
this.onQueryTextChangeListener = onQueryTextChangeListener; | |
} | |
public void setImeOptions(int imeOptions) { | |
searchView.setImeOptions(imeOptions); | |
} | |
public void setOnEditorActionListener(TextView.OnEditorActionListener onEditorActionListener) { | |
this.onEditorActionListener = onEditorActionListener; | |
} | |
public void setOnListAnimationListener(OnSuggestionsAnimationListener onListAnimationListener) { | |
this.onListAnimationListener = onListAnimationListener; | |
} | |
public boolean isExpanded() { | |
return suggestionListContainer.getVisibility() == VISIBLE; | |
} | |
void computeAnimationIn() { | |
endListHeight = 0; | |
startListHeight = suggestionListContainer.getHeight(); | |
ViewGroup.LayoutParams layoutParams = suggestionListContainer.getLayoutParams(); | |
layoutParams.height = 0; | |
suggestionListContainer.setLayoutParams(layoutParams); | |
suggestionsContainer.getViewTreeObserver().addOnGlobalLayoutListener( | |
new ViewTreeObserver.OnGlobalLayoutListener() { | |
@Override | |
public void onGlobalLayout() { | |
if (waitKeyboardForCollapse | |
&& (isWindowAdjustPanMode() | |
? getKeyboardHeight() <= 0 | |
: suggestionsContainer.getHeight() == startListHeight) | |
) { | |
return; | |
} | |
ViewTreeObserver vto = suggestionsContainer.getViewTreeObserver(); | |
if (vto.isAlive()) { | |
vto.removeOnGlobalLayoutListener(this); | |
} | |
// Confronta l'altezza iniziale con l'altezza del container | |
// (resizato dalla tastiera se possibile) | |
if (startListHeight > suggestionsContainer.getHeight()) { | |
// animazione solo fino a fine container | |
endListHeight = suggestionsContainer.getHeight(); | |
} else { | |
// l'altezza iniziale non ha superato il container | |
if (isWindowAdjustPanMode()) { | |
// se siamo in ADJUST_PAN, calcoliamo la size della tastiera | |
int keyboardHeight = getKeyboardHeight(); | |
MarginLayoutParams marginLayoutParams = | |
(MarginLayoutParams) getLayoutParams(); | |
marginLayoutParams.bottomMargin = keyboardHeight; | |
setLayoutParams(marginLayoutParams); | |
endListHeight = startListHeight > keyboardHeight | |
? startListHeight - keyboardHeight | |
: startListHeight; | |
} else { | |
endListHeight = startListHeight; | |
} | |
} | |
animateListContainer(); | |
} | |
}); | |
} | |
void setFocusInternally(boolean isFocused) { | |
this.isFocused = isFocused; | |
if (isFocused) { | |
searchView.requestFocus(); | |
searchView.setLongClickable(true); | |
searchView.setEllipsize(null); | |
searchView.setKeyListener(keyListener); | |
Utils.showKeyboard(searchView); | |
if (onFocusChangeListener != null) { | |
onFocusChangeListener.onFocus(); | |
} | |
if (delayAnimationIn > 0) { | |
getHandler().postDelayed(this::computeAnimationIn, delayAnimationIn); | |
} else { | |
computeAnimationIn(); | |
} | |
} else { | |
searchView.clearFocus(); | |
searchView.setLongClickable(false); | |
searchView.setEllipsize(TextUtils.TruncateAt.END); | |
searchView.setKeyListener(null); | |
Utils.hideKeyboard(searchView); | |
getHandler().removeCallbacksAndMessages(null); | |
if (clearTextOnDismiss) { | |
clearText(); | |
} | |
if (onFocusChangeListener != null) { | |
onFocusChangeListener.onFocusCleared(); | |
} | |
animateListContainer(); | |
} | |
} | |
private void initBackButton(boolean btnBackVisible) { | |
btnBack.setVisibility(btnBackVisible ? VISIBLE : GONE); | |
if (btnBackVisible) { | |
btnBack.setOnClickListener(v -> { | |
if (isFocused) { | |
setFocusInternally(false); | |
} | |
if (onBackClickListener != null) { | |
onBackClickListener.onClick(v); | |
} | |
}); | |
} else { | |
MarginLayoutParams layoutParams = (MarginLayoutParams) searchView.getLayoutParams(); | |
layoutParams.setMarginStart(dimen16); | |
} | |
} | |
public void clearText() { | |
searchView.setText(null); | |
} | |
void animate(int fromValue, int toValue) { | |
final boolean animateIn = toValue > fromValue; | |
valueAnimator = ValueAnimator.ofInt(fromValue, toValue); | |
valueAnimator.setInterpolator(ANIM_INTERPOLATOR); | |
valueAnimator.addUpdateListener(animation -> { | |
setListHeight((int) animation.getAnimatedValue()); | |
}); | |
valueAnimator.addListener(new AnimatorListenerAdapter() { | |
@Override | |
public void onAnimationStart(Animator animation) { | |
isAnimating = true; | |
if (animateIn) { | |
suggestionListContainer.getLayoutParams().height = 0; | |
suggestionListContainer.setVisibility(VISIBLE); | |
} | |
toggleDividerView(animateIn); | |
} | |
@Override | |
public void onAnimationEnd(Animator animation) { | |
isAnimating = false; | |
if (animateIn) { | |
if (onListAnimationListener != null) { | |
onListAnimationListener.onSuggestionsIn(); | |
} | |
if (waitKeyboardForCollapse) { | |
// aggiungi un ritardo in modo che non glitcha | |
// se ancora non è apparsa la tastiera | |
new Handler().postDelayed( | |
() -> setListHeight(ViewGroup.LayoutParams.WRAP_CONTENT), | |
100 | |
); | |
} else { | |
setListHeight(ViewGroup.LayoutParams.WRAP_CONTENT); | |
} | |
} else { | |
suggestionListContainer.setVisibility(INVISIBLE); | |
suggestionListContainer.getLayoutParams().height = | |
ViewGroup.LayoutParams.WRAP_CONTENT; | |
if (onListAnimationListener != null) { | |
onListAnimationListener.onSuggestionsOut(); | |
} | |
if (isWindowAdjustPanMode()) { | |
MarginLayoutParams marginLayoutParams = | |
(MarginLayoutParams) getLayoutParams(); | |
marginLayoutParams.bottomMargin = 0; | |
setLayoutParams(marginLayoutParams); | |
} | |
} | |
} | |
}); | |
valueAnimator.setDuration(ANIM_DURATION).start(); | |
} | |
AppCompatActivity getActivity() { | |
return ((AppCompatActivity) getContext()); | |
} | |
int getKeyboardHeight() { | |
Rect visibleRect = new Rect(); | |
if (activityRootView == null) { | |
activityRootView = getActivityRoot(getActivity()); | |
} | |
activityRootView.getWindowVisibleDisplayFrame(visibleRect); | |
return activityRootView.getRootView().getHeight() - visibleRect.bottom - bottomInsets; | |
} | |
View getActivityRoot(@NonNull Activity activity) { | |
return ((ViewGroup) activity.findViewById(android.R.id.content)).getChildAt(0); | |
} | |
boolean isWindowAdjustPanMode() { | |
return isAdjustPanMode; | |
} | |
void setListHeight(int height) { | |
// if (height == ViewGroup.LayoutParams.WRAP_CONTENT && isWindowAdjustPanMode()) { | |
// return; | |
// } | |
suggestionListContainer.getLayoutParams().height = height; | |
suggestionListContainer.requestLayout(); | |
} | |
private void removePendingAnimation() { | |
if (valueAnimator != null && valueAnimator.isRunning()) { | |
Logger.d(TAG, "Removing pending list animation"); | |
valueAnimator.cancel(); | |
} | |
} | |
private void removePendingDataObserver() { | |
if (hasAdapter()) { | |
suggestionsList.getAdapter().unregisterAdapterDataObserver(adapterObserver); | |
} | |
} | |
void animateListContainer() { | |
// if (isAnimating) { | |
// return; | |
// } | |
if (isOptionsIconEnabled) { | |
toggleOptionsButtonVisibility(!isFocused); | |
} | |
int fromValue; | |
int toValue; | |
if (isExpanded()) { | |
// animateOut | |
// int listHeight = suggestionListContainer.getHeight(); | |
// from = listHeight < endListHeight ? listHeight : endListHeight; | |
fromValue = suggestionListContainer.getHeight(); | |
toValue = 0; | |
} else { | |
// animateIn | |
fromValue = 0; | |
toValue = endListHeight; | |
} | |
animate(fromValue, toValue); | |
} | |
boolean hasAdapter() { | |
return suggestionsList.getAdapter() != null; | |
} | |
int getItemCount() { | |
return suggestionsList.getAdapter() != null | |
? suggestionsList.getAdapter().getItemCount() | |
: 0; | |
} | |
private float calculateVerticalPadding(CardView cardView) { | |
float maxCardElevation = cardView.getMaxCardElevation(); | |
float cornerRadius = cardView.getRadius(); | |
boolean addPaddingForCorners = cardView.getPreventCornerOverlap(); | |
if (addPaddingForCorners) { | |
return (float) (maxCardElevation * 1.5f + (1 - COS_45) * cornerRadius); | |
} else { | |
return maxCardElevation * 1.5f; | |
} | |
} | |
private float calculateHorizontalPadding(CardView cardView) { | |
float maxCardElevation = cardView.getMaxCardElevation(); | |
float cornerRadius = cardView.getRadius(); | |
boolean addPaddingForCorners = cardView.getPreventCornerOverlap(); | |
if (addPaddingForCorners) { | |
return (float) (maxCardElevation + (1 - COS_45) * cornerRadius); | |
} else { | |
return maxCardElevation; | |
} | |
} | |
private void initSuggestionsContainer() { | |
float cardViewBottomPadding = | |
Utils.pxFromDp(getContext(), CARD_VIEW_CORNERS_AND_TOP_BOTTOM_SHADOW_HEIGHT); | |
// float cardViewBottomPadding = | |
// (float) Math.ceil(calculateVerticalPadding(suggestionListContainer)); | |
// move up the suggestions section enough to cover the search bar | |
// card's bottom left and right corners | |
suggestionsContainer.setTranslationY(-cardViewBottomPadding); | |
} | |
private void initSuggestionsListContainer() { | |
float cardViewBottomPadding = | |
Utils.pxFromDp(getContext(), CARD_VIEW_CORNERS_AND_TOP_BOTTOM_SHADOW_HEIGHT); | |
// float cardViewBottomPadding = | |
// (float) Math.ceil(calculateVerticalPadding(suggestionListContainer)); | |
suggestionListContainer.setTranslationY(-cardViewBottomPadding); | |
} | |
private void initSuggestionsList() { | |
suggestionsList.setLayoutManager(new LinearLayoutManager(getContext())); | |
} | |
private void initClearBtn() { | |
btnClear.setOnClickListener(v -> { | |
clearText(); | |
if (!isFocused) { | |
setFocusInternally(true); | |
} | |
if (onBtnClearClickListener != null) { | |
onBtnClearClickListener.onClick(v); | |
} | |
}); | |
} | |
private void initSearchInputView() { | |
keyListener = searchView.getKeyListener(); | |
searchView.setOnFocusChangeListener((v, hasFocus) -> { | |
if ((hasFocus != isFocused) && isEnabled()) { | |
setFocusInternally(hasFocus); | |
} | |
}); | |
searchView.setOnEditorActionListener((v, actionId, event) -> { | |
boolean isEnterEvent = event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER; | |
boolean isActionDone = actionId == EditorInfo.IME_ACTION_DONE; | |
if (isActionDone || isEnterEvent) { | |
setFocusInternally(false); | |
} | |
return onEditorActionListener != null | |
&& onEditorActionListener.onEditorAction(v, actionId, event); | |
}); | |
searchView.setOnKeyboardDismissedListener(() -> setFocusInternally(false)); | |
textWatcher = new TextWatcherAdapter() { | |
@Override | |
public void onTextChanged(CharSequence text, int start, int before, int count) { | |
if (skipTextChangeEvent) { | |
skipTextChangeEvent = false; | |
} else { | |
boolean enableCancelBtn = !TextUtils.isEmpty(text); | |
toggleCancelButtonVisibility(enableCancelBtn); | |
if (onQueryTextChangeListener != null) { | |
onQueryTextChangeListener.onQueryChanged(text); | |
} | |
} | |
} | |
}; | |
searchView.addTextChangedListener(textWatcher); | |
} | |
void toggleOptionsButtonVisibility(boolean visible) { | |
int visibility = visible ? VISIBLE : GONE; | |
btnOptions.setVisibility(visibility); | |
separatorView.setVisibility(visibility); | |
} | |
void toggleCancelButtonVisibility(boolean visible) { | |
btnClear.setVisibility(visible ? VISIBLE : GONE); | |
} | |
@Nullable | |
@Override | |
protected Parcelable onSaveInstanceState() { | |
Parcelable superState = super.onSaveInstanceState(); | |
return new SavedState(superState); | |
} | |
@Override | |
protected void onRestoreInstanceState(Parcelable state) { | |
SavedState savedState = (SavedState) state; | |
super.onRestoreInstanceState(savedState.getSuperState()); | |
skipTextChangeEvent = true; | |
} | |
private static class SavedState extends BaseSavedState { | |
boolean isFocused; | |
SavedState(Parcelable superState) { | |
super(superState); | |
} | |
SavedState(Parcel in) { | |
super(in); | |
isFocused = (in.readInt() != 0); | |
} | |
@Override | |
public void writeToParcel(Parcel out, int flags) { | |
super.writeToParcel(out, flags); | |
out.writeInt(isFocused ? 1 : 0); | |
} | |
public static final Creator<SavedState> CREATOR = new Creator<SavedState>() { | |
@Override | |
public SavedState createFromParcel(Parcel in) { | |
return new SavedState(in); | |
} | |
@Override | |
public SavedState[] newArray(int size) { | |
return new SavedState[size]; | |
} | |
}; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @author Simone Bellotti | |
*/ | |
public class TextWatcherAdapter implements TextWatcher { | |
@Override | |
public void beforeTextChanged(CharSequence text, int start, int count, int after) { | |
} | |
@Override | |
public void onTextChanged(CharSequence text, int start, int before, int count) { | |
} | |
@Override | |
public void afterTextChanged(Editable text) { | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class Utils { | |
public static void showKeyboard(@Nullable EditText editText) { | |
if (editText != null) { | |
editText.requestFocus(); | |
InputMethodManager imm = | |
(InputMethodManager) editText.getContext() | |
.getSystemService(Context.INPUT_METHOD_SERVICE); | |
// imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT); | |
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); | |
} | |
} | |
public static void hideKeyboard(@Nullable EditText editText) { | |
if (editText != null) { | |
editText.clearFocus(); | |
InputMethodManager imm = | |
(InputMethodManager) editText.getContext() | |
.getSystemService(Context.INPUT_METHOD_SERVICE); | |
imm.hideSoftInputFromWindow(editText.getWindowToken(), 0); | |
} | |
} | |
public static float pxFromDp(Context ctx, float dp) { | |
return dp * ctx.getResources().getDisplayMetrics().density; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment