/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.plaidapp.ui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.InputType;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.transition.Transition;
import android.transition.TransitionInflater;
import android.transition.TransitionManager;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.ViewTreeObserver;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.EditorInfo;
import android.widget.CheckBox;
import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.SearchView;
import java.util.List;
import butterknife.Bind;
import butterknife.BindDimen;
import butterknife.BindInt;
import butterknife.ButterKnife;
import butterknife.OnClick;
import io.plaidapp.R;
import io.plaidapp.data.PlaidItem;
import io.plaidapp.data.SearchDataManager;
import io.plaidapp.data.pocket.PocketUtils;
import io.plaidapp.ui.recyclerview.InfiniteScrollListener;
import io.plaidapp.ui.widget.BaselineGridTextView;
import io.plaidapp.util.ImeUtils;
import io.plaidapp.util.ViewUtils;
public class SearchActivity extends Activity {
public static final String EXTRA_MENU_LEFT = "EXTRA_MENU_LEFT";
public static final String EXTRA_MENU_CENTER_X = "EXTRA_MENU_CENTER_X";
public static final String EXTRA_QUERY = "EXTRA_QUERY";
public static final String EXTRA_SAVE_DRIBBBLE = "EXTRA_SAVE_DRIBBBLE";
public static final String EXTRA_SAVE_DESIGNER_NEWS = "EXTRA_SAVE_DESIGNER_NEWS";
public static final int RESULT_CODE_SAVE = 7;
@Bind(R.id.searchback) ImageButton searchBack;
@Bind(R.id.searchback_container) ViewGroup searchBackContainer;
@Bind(R.id.search_view) SearchView searchView;
@Bind(R.id.search_background) View searchBackground;
@Bind(android.R.id.empty) ProgressBar progress;
@Bind(R.id.search_results) RecyclerView results;
@Bind(R.id.container) ViewGroup container;
@Bind(R.id.search_toolbar) ViewGroup searchToolbar;
@Bind(R.id.results_container) ViewGroup resultsContainer;
@Bind(R.id.fab) ImageButton fab;
@Bind(R.id.confirm_save_container) ViewGroup confirmSaveContainer;
@Bind(R.id.save_dribbble) CheckBox saveDribbble;
@Bind(R.id.save_designer_news) CheckBox saveDesignerNews;
@Bind(R.id.scrim) View scrim;
@Bind(R.id.results_scrim) View resultsScrim;
private BaselineGridTextView noResults;
@BindInt(R.integer.num_columns) int columns;
@BindDimen(R.dimen.z_app_bar) float appBarElevation;
private Transition auto;
private int searchBackDistanceX;
private int searchIconCenterX;
private SearchDataManager dataManager;
private FeedAdapter adapter;
public static Intent createStartIntent(Context context, int menuIconLeft, int menuIconCenterX) {
Intent starter = new Intent(context, SearchActivity.class);
starter.putExtra(EXTRA_MENU_LEFT, menuIconLeft);
starter.putExtra(EXTRA_MENU_CENTER_X, menuIconCenterX);
return starter;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
ButterKnife.bind(this);
setupSearchView();
auto = TransitionInflater.from(this).inflateTransition(R.transition.auto);
dataManager = new SearchDataManager(this) {
@Override
public void onDataLoaded(List<? extends PlaidItem> data) {
if (data != null && data.size() > 0) {
if (results.getVisibility() != View.VISIBLE) {
TransitionManager.beginDelayedTransition(container, auto);
progress.setVisibility(View.GONE);
results.setVisibility(View.VISIBLE);
fab.setVisibility(View.VISIBLE);
fab.setAlpha(0.6f);
fab.setScaleX(0f);
fab.setScaleY(0f);
fab.animate()
.alpha(1f)
.scaleX(1f)
.scaleY(1f)
.setStartDelay(800L)
.setDuration(300L)
.setInterpolator(AnimationUtils.loadInterpolator(SearchActivity
.this, android.R.interpolator.linear_out_slow_in));
}
adapter.addAndResort(data);
} else {
TransitionManager.beginDelayedTransition(container, auto);
progress.setVisibility(View.GONE);
setNoResultsVisibility(View.VISIBLE);
}
}
};
adapter = new FeedAdapter(this, dataManager, columns, PocketUtils.isPocketInstalled(this));
results.setAdapter(adapter);
GridLayoutManager layoutManager = new GridLayoutManager(this, columns);
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return adapter.getItemColumnSpan(position);
}
});
results.setLayoutManager(layoutManager);
results.addOnScrollListener(new InfiniteScrollListener(layoutManager, dataManager) {
@Override
public void onLoadMore() {
dataManager.loadMore();
}
});
results.setHasFixedSize(true);
results.addOnScrollListener(gridScroll);
// extract the search icon's location passed from the launching activity, minus 4dp to
// compensate for different paddings in the views
searchBackDistanceX = getIntent().getIntExtra(EXTRA_MENU_LEFT, 0) - (int) TypedValue
.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, getResources().getDisplayMetrics());
searchIconCenterX = getIntent().getIntExtra(EXTRA_MENU_CENTER_X, 0);
// translate icon to match the launching screen then animate back into position
searchBackContainer.setTranslationX(searchBackDistanceX);
searchBackContainer.animate()
.translationX(0f)
.setDuration(650L)
.setInterpolator(AnimationUtils.loadInterpolator(this,
android.R.interpolator.fast_out_slow_in));
// transform from search icon to back icon
AnimatedVectorDrawable searchToBack = (AnimatedVectorDrawable) ContextCompat
.getDrawable(this, R.drawable.avd_search_to_back);
searchBack.setImageDrawable(searchToBack);
searchToBack.start();
// for some reason the animation doesn't always finish (leaving a part arrow!?) so after
// the animation set a static drawable. Also animation callbacks weren't added until API23
// so using post delayed :(
// TODO fix properly!!
searchBack.postDelayed(new Runnable() {
@Override
public void run() {
searchBack.setImageDrawable(ContextCompat.getDrawable(SearchActivity.this,
R.drawable.ic_arrow_back_padded));
}
}, 600);
// fade in the other search chrome
searchBackground.animate()
.alpha(1f)
.setDuration(300L)
.setInterpolator(AnimationUtils.loadInterpolator(this,
android.R.interpolator.linear_out_slow_in));
searchView.animate()
.alpha(1f)
.setStartDelay(400L)
.setDuration(400L)
.setInterpolator(AnimationUtils.loadInterpolator(this,
android.R.interpolator.linear_out_slow_in))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
searchView.requestFocus();
ImeUtils.showIme(searchView);
}
});
// animate in a scrim over the content behind
scrim.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
scrim.getViewTreeObserver().removeOnPreDrawListener(this);
AnimatorSet showScrim = new AnimatorSet();
showScrim.playTogether(
ViewAnimationUtils.createCircularReveal(
scrim,
searchIconCenterX,
searchBackground.getBottom(),
0,
(float) Math.hypot(searchBackDistanceX, scrim.getHeight()
- searchBackground.getBottom())),
ObjectAnimator.ofArgb(
scrim,
ViewUtils.BACKGROUND_COLOR,
Color.TRANSPARENT,
ContextCompat.getColor(SearchActivity.this, R.color.scrim)));
showScrim.setDuration(400L);
showScrim.setInterpolator(AnimationUtils.loadInterpolator(SearchActivity.this,
android.R.interpolator.linear_out_slow_in));
showScrim.start();
return false;
}
});
onNewIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
if (intent.hasExtra(SearchManager.QUERY)) {
String query = intent.getStringExtra(SearchManager.QUERY);
if (!TextUtils.isEmpty(query)) {
searchView.setQuery(query, false);
searchFor(query);
}
}
}
@Override
public void onBackPressed() {
if (confirmSaveContainer.getVisibility() == View.VISIBLE) {
hideSaveConfimation();
} else {
dismiss();
}
}
@Override
protected void onPause() {
// needed to suppress the default window animation when closing the activity
overridePendingTransition(0, 0);
super.onPause();
}
@OnClick({ R.id.scrim, R.id.searchback })
protected void dismiss() {
// translate the icon to match position in the launching activity
searchBackContainer.animate()
.translationX(searchBackDistanceX)
.setDuration(600L)
.setInterpolator(AnimationUtils.loadInterpolator(this,
android.R.interpolator.fast_out_slow_in))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
finishAfterTransition();
}
})
.start();
// transform from back icon to search icon
AnimatedVectorDrawable backToSearch = (AnimatedVectorDrawable) ContextCompat
.getDrawable(this, R.drawable.avd_back_to_search);
searchBack.setImageDrawable(backToSearch);
// clear the background else the touch ripple moves with the translation which looks bad
searchBack.setBackground(null);
backToSearch.start();
// fade out the other search chrome
searchView.animate()
.alpha(0f)
.setStartDelay(0L)
.setDuration(120L)
.setInterpolator(AnimationUtils.loadInterpolator(this,
android.R.interpolator.fast_out_linear_in))
.setListener(null)
.start();
searchBackground.animate()
.alpha(0f)
.setStartDelay(300L)
.setDuration(160L)
.setInterpolator(AnimationUtils.loadInterpolator(this,
android.R.interpolator.fast_out_linear_in))
.setListener(null)
.start();
if (searchToolbar.getZ() != 0f) {
searchToolbar.animate()
.z(0f)
.setDuration(600L)
.setInterpolator(AnimationUtils.loadInterpolator(this,
android.R.interpolator.fast_out_linear_in))
.start();
}
// if we're showing search results, circular hide them
if (resultsContainer.getHeight() > 0) {
Animator closeResults = ViewAnimationUtils.createCircularReveal(
resultsContainer,
searchIconCenterX,
0,
(float) Math.hypot(searchIconCenterX, resultsContainer.getHeight()),
0f);
closeResults.setDuration(500L);
closeResults.setInterpolator(AnimationUtils.loadInterpolator(SearchActivity.this,
android.R.interpolator.fast_out_slow_in));
closeResults.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
resultsContainer.setVisibility(View.INVISIBLE);
}
});
closeResults.start();
}
// fade out the scrim
scrim.animate()
.alpha(0f)
.setDuration(400L)
.setInterpolator(AnimationUtils.loadInterpolator(this,
android.R.interpolator.fast_out_linear_in))
.setListener(null)
.start();
}
@OnClick(R.id.fab)
protected void save() {
// show the save confirmation bubble
fab.setVisibility(View.INVISIBLE);
confirmSaveContainer.setVisibility(View.VISIBLE);
resultsScrim.setVisibility(View.VISIBLE);
// expand it once it's been measured and show a scrim over the search results
confirmSaveContainer.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver
.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
// expand the confirmation
confirmSaveContainer.getViewTreeObserver().removeOnPreDrawListener(this);
Animator reveal = ViewAnimationUtils.createCircularReveal(confirmSaveContainer,
confirmSaveContainer.getWidth() / 2,
confirmSaveContainer.getHeight() / 2,
fab.getWidth() / 2,
confirmSaveContainer.getWidth() / 2);
reveal.setDuration(250L);
reveal.setInterpolator(AnimationUtils.loadInterpolator(SearchActivity.this,
android.R.interpolator.fast_out_slow_in));
reveal.start();
// show the scrim
int centerX = (fab.getLeft() + fab.getRight()) / 2;
int centerY = (fab.getTop() + fab.getBottom()) / 2;
Animator revealScrim = ViewAnimationUtils.createCircularReveal(
resultsScrim,
centerX,
centerY,
0,
(float) Math.hypot(centerX, centerY));
revealScrim.setDuration(400L);
revealScrim.setInterpolator(AnimationUtils.loadInterpolator(SearchActivity
.this, android.R.interpolator.linear_out_slow_in));
revealScrim.start();
ObjectAnimator fadeInScrim = ObjectAnimator.ofArgb(resultsScrim,
ViewUtils.BACKGROUND_COLOR,
Color.TRANSPARENT,
ContextCompat.getColor(SearchActivity.this, R.color.scrim));
fadeInScrim.setDuration(800L);
fadeInScrim.setInterpolator(AnimationUtils.loadInterpolator(SearchActivity
.this, android.R.interpolator.linear_out_slow_in));
fadeInScrim.start();
// ease in the checkboxes
saveDribbble.setAlpha(0.6f);
saveDribbble.setTranslationY(saveDribbble.getHeight() * 0.4f);
saveDribbble.animate()
.alpha(1f)
.translationY(0f)
.setDuration(200L)
.setInterpolator(AnimationUtils.loadInterpolator(SearchActivity.this,
android.R.interpolator.linear_out_slow_in));
saveDesignerNews.setAlpha(0.6f);
saveDesignerNews.setTranslationY(saveDesignerNews.getHeight() * 0.5f);
saveDesignerNews.animate()
.alpha(1f)
.translationY(0f)
.setDuration(200L)
.setInterpolator(AnimationUtils.loadInterpolator(SearchActivity.this,
android.R.interpolator.linear_out_slow_in));
return false;
}
});
}
@OnClick(R.id.save_confirmed)
protected void doSave() {
Intent saveData = new Intent();
saveData.putExtra(EXTRA_QUERY, dataManager.getQuery());
saveData.putExtra(EXTRA_SAVE_DRIBBBLE, saveDribbble.isChecked());
saveData.putExtra(EXTRA_SAVE_DESIGNER_NEWS, saveDesignerNews.isChecked());
setResult(RESULT_CODE_SAVE, saveData);
dismiss();
}
@OnClick(R.id.results_scrim)
protected void hideSaveConfimation() {
if (confirmSaveContainer.getVisibility() == View.VISIBLE) {
// contract the bubble & hide the scrim
AnimatorSet hideConfirmation = new AnimatorSet();
hideConfirmation.playTogether(
ViewAnimationUtils.createCircularReveal(confirmSaveContainer,
confirmSaveContainer.getWidth() / 2,
confirmSaveContainer.getHeight() / 2,
confirmSaveContainer.getWidth() / 2,
fab.getWidth() / 2),
ObjectAnimator.ofArgb(resultsScrim,
ViewUtils.BACKGROUND_COLOR,
Color.TRANSPARENT));
hideConfirmation.setDuration(150L);
hideConfirmation.setInterpolator(AnimationUtils.loadInterpolator(SearchActivity.this,
android.R.interpolator.fast_out_slow_in));
hideConfirmation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
confirmSaveContainer.setVisibility(View.GONE);
resultsScrim.setVisibility(View.GONE);
fab.setVisibility(results.getVisibility());
}
});
hideConfirmation.start();
}
}
private void setupSearchView() {
SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
// hint, inputType & ime options seem to be ignored from XML! Set in code
searchView.setQueryHint(getString(R.string.search_hint));
searchView.setInputType(InputType.TYPE_TEXT_FLAG_CAP_WORDS);
searchView.setImeOptions(searchView.getImeOptions() | EditorInfo.IME_ACTION_SEARCH |
EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_FLAG_NO_FULLSCREEN);
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
searchFor(query);
return true;
}
@Override
public boolean onQueryTextChange(String query) {
if (TextUtils.isEmpty(query)) {
clearResults();
}
return true;
}
});
searchView.setOnQueryTextFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus && confirmSaveContainer.getVisibility() == View.VISIBLE) {
hideSaveConfimation();
}
}
});
}
private void clearResults() {
adapter.clear();
dataManager.clear();
TransitionManager.beginDelayedTransition(container, auto);
results.setVisibility(View.GONE);
progress.setVisibility(View.GONE);
fab.setVisibility(View.GONE);
confirmSaveContainer.setVisibility(View.GONE);
resultsScrim.setVisibility(View.GONE);
setNoResultsVisibility(View.GONE);
}
private void setNoResultsVisibility(int visibility) {
if (visibility == View.VISIBLE) {
if (noResults == null) {
noResults = (BaselineGridTextView) ((ViewStub)
findViewById(R.id.stub_no_search_results)).inflate();
noResults.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
searchView.setQuery("", false);
searchView.requestFocus();
ImeUtils.showIme(searchView);
}
});
}
String message = String.format(getString(R
.string.no_search_results), searchView.getQuery().toString());
SpannableStringBuilder ssb = new SpannableStringBuilder(message);
ssb.setSpan(new StyleSpan(Typeface.ITALIC),
message.indexOf('“') + 1,
message.length() - 1,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
noResults.setText(ssb);
}
if (noResults != null) {
noResults.setVisibility(visibility);
}
}
private void searchFor(String query) {
clearResults();
progress.setVisibility(View.VISIBLE);
ImeUtils.hideIme(searchView);
searchView.clearFocus();
dataManager.searchFor(query);
}
private int gridScrollY = 0;
private RecyclerView.OnScrollListener gridScroll = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
gridScrollY += dy;
if (gridScrollY > 0 && searchToolbar.getTranslationZ() != appBarElevation) {
searchToolbar.animate()
.translationZ(appBarElevation)
.setDuration(300L)
.setInterpolator(AnimationUtils.loadInterpolator(SearchActivity.this,
android.R.interpolator.fast_out_slow_in))
.start();
} else if (gridScrollY == 0 && searchToolbar.getTranslationZ() != 0) {
searchToolbar.animate()
.translationZ(0f)
.setDuration(300L)
.setInterpolator(AnimationUtils.loadInterpolator(SearchActivity.this,
android.R.interpolator.fast_out_slow_in))
.start();
}
}
};
}