From 210dc82b9392d4f4999e1c87e6cfcd6a553f8270 Mon Sep 17 00:00:00 2001 From: Nick Butcher Date: Mon, 15 Aug 2016 18:04:14 +0100 Subject: [PATCH] Transition all the things. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit – Switch SearchActivity animations to use transitions. – Introduce StartAnimatable & CircularReveal transitions. --- .../io/plaidapp/ui/DesignerNewsLogin.java | 5 +- .../java/io/plaidapp/ui/DribbbleShot.java | 4 +- .../main/java/io/plaidapp/ui/FeedAdapter.java | 4 +- .../java/io/plaidapp/ui/HomeActivity.java | 10 +- .../java/io/plaidapp/ui/SearchActivity.java | 348 ++++-------------- .../ui/transitions/CircularReveal.java | 148 ++++++++ .../ui/transitions/StartAnimatable.java | 92 +++++ .../main/java/io/plaidapp/util/AnimUtils.java | 28 -- .../io/plaidapp/util/TransitionUtils.java | 93 +++++ .../res/drawable/ic_arrow_back_padded.xml | 13 +- app/src/main/res/drawable/searchback_back.xml | 11 +- .../main/res/drawable/searchback_search.xml | 11 +- app/src/main/res/layout/activity_search.xml | 15 +- app/src/main/res/transition/auto.xml | 4 +- app/src/main/res/transition/search_enter.xml | 46 ++- .../res/transition/search_hide_confirm.xml | 40 ++ app/src/main/res/transition/search_return.xml | 62 ++++ .../res/transition/search_shared_enter.xml | 31 ++ .../res/transition/search_shared_return.xml | 31 ++ .../res/transition/search_show_confirm.xml | 44 +++ .../res/transition/search_show_results.xml | 34 ++ .../main/res/values/attrs_circular_reveal.xml | 26 ++ .../res/values/attrs_start_animatable.xml | 24 ++ app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/styles.xml | 4 + 26 files changed, 777 insertions(+), 353 deletions(-) create mode 100644 app/src/main/java/io/plaidapp/ui/transitions/CircularReveal.java create mode 100644 app/src/main/java/io/plaidapp/ui/transitions/StartAnimatable.java create mode 100644 app/src/main/java/io/plaidapp/util/TransitionUtils.java create mode 100644 app/src/main/res/transition/search_hide_confirm.xml create mode 100644 app/src/main/res/transition/search_return.xml create mode 100644 app/src/main/res/transition/search_shared_enter.xml create mode 100644 app/src/main/res/transition/search_shared_return.xml create mode 100644 app/src/main/res/transition/search_show_confirm.xml create mode 100644 app/src/main/res/transition/search_show_results.xml create mode 100644 app/src/main/res/values/attrs_circular_reveal.xml create mode 100644 app/src/main/res/values/attrs_start_animatable.xml diff --git a/app/src/main/java/io/plaidapp/ui/DesignerNewsLogin.java b/app/src/main/java/io/plaidapp/ui/DesignerNewsLogin.java index 674a68af0..bd07dfadc 100644 --- a/app/src/main/java/io/plaidapp/ui/DesignerNewsLogin.java +++ b/app/src/main/java/io/plaidapp/ui/DesignerNewsLogin.java @@ -70,8 +70,8 @@ import io.plaidapp.data.prefs.DesignerNewsPrefs; import io.plaidapp.ui.transitions.FabTransform; import io.plaidapp.ui.transitions.MorphTransform; -import io.plaidapp.util.AnimUtils; import io.plaidapp.util.ScrimUtil; +import io.plaidapp.util.TransitionUtils; import io.plaidapp.util.glide.CircleTransform; import retrofit2.Call; import retrofit2.Callback; @@ -107,8 +107,7 @@ protected void onCreate(Bundle savedInstanceState) { getResources().getDimensionPixelSize(R.dimen.dialog_corners)); } if (getWindow().getSharedElementEnterTransition() != null) { - getWindow().getSharedElementEnterTransition().addListener(new AnimUtils - .TransitionListenerAdapter() { + getWindow().getSharedElementEnterTransition().addListener(new TransitionUtils.TransitionListenerAdapter() { @Override public void onTransitionEnd(Transition transition) { finishSetup(); diff --git a/app/src/main/java/io/plaidapp/ui/DribbbleShot.java b/app/src/main/java/io/plaidapp/ui/DribbbleShot.java index 008e7b15f..b2b350e25 100644 --- a/app/src/main/java/io/plaidapp/ui/DribbbleShot.java +++ b/app/src/main/java/io/plaidapp/ui/DribbbleShot.java @@ -91,10 +91,10 @@ import io.plaidapp.ui.widget.FabOverlapTextView; import io.plaidapp.ui.widget.ForegroundImageView; import io.plaidapp.ui.widget.ParallaxScrimageView; -import io.plaidapp.util.AnimUtils; import io.plaidapp.util.ColorUtils; import io.plaidapp.util.HtmlUtils; import io.plaidapp.util.ImeUtils; +import io.plaidapp.util.TransitionUtils; import io.plaidapp.util.ViewOffsetHelper; import io.plaidapp.util.ViewUtils; import io.plaidapp.util.customtabs.CustomTabActivityHelper; @@ -594,7 +594,7 @@ public void onClick(View view) { }; private Transition.TransitionListener shotReturnHomeListener = - new AnimUtils.TransitionListenerAdapter() { + new TransitionUtils.TransitionListenerAdapter() { @Override public void onTransitionStart(Transition transition) { super.onTransitionStart(transition); diff --git a/app/src/main/java/io/plaidapp/ui/FeedAdapter.java b/app/src/main/java/io/plaidapp/ui/FeedAdapter.java index 3e23b30f3..bc945320f 100644 --- a/app/src/main/java/io/plaidapp/ui/FeedAdapter.java +++ b/app/src/main/java/io/plaidapp/ui/FeedAdapter.java @@ -78,8 +78,8 @@ import io.plaidapp.data.prefs.SourceManager; import io.plaidapp.ui.transitions.ReflowText; import io.plaidapp.ui.widget.BadgedFourThreeImageView; -import io.plaidapp.util.AnimUtils; import io.plaidapp.util.ObservableColorMatrix; +import io.plaidapp.util.TransitionUtils; import io.plaidapp.util.ViewUtils; import io.plaidapp.util.customtabs.CustomTabActivityHelper; import io.plaidapp.util.glide.DribbbleTarget; @@ -615,7 +615,7 @@ private void setGridItemContentTransitions(View gridItem) { Transition reenter = TransitionInflater.from(host) .inflateTransition(R.transition.grid_overlap_fab_reenter); - reenter.addListener(new AnimUtils.TransitionListenerAdapter() { + reenter.addListener(new TransitionUtils.TransitionListenerAdapter() { @Override public void onTransitionEnd(Transition transition) { diff --git a/app/src/main/java/io/plaidapp/ui/HomeActivity.java b/app/src/main/java/io/plaidapp/ui/HomeActivity.java index d4a2da0b7..8983c8164 100644 --- a/app/src/main/java/io/plaidapp/ui/HomeActivity.java +++ b/app/src/main/java/io/plaidapp/ui/HomeActivity.java @@ -337,14 +337,10 @@ public boolean onOptionsItemSelected(MenuItem item) { drawer.openDrawer(GravityCompat.END); return true; case R.id.menu_search: - // get the icon's location on screen to pass through to the search screen View searchMenuView = toolbar.findViewById(R.id.menu_search); - int[] loc = new int[2]; - searchMenuView.getLocationOnScreen(loc); - startActivityForResult(SearchActivity.createStartIntent(this, loc[0], loc[0] + - (searchMenuView.getWidth() / 2)), RC_SEARCH, ActivityOptions - .makeSceneTransitionAnimation(this).toBundle()); - searchMenuView.setAlpha(0f); + Bundle options = ActivityOptions.makeSceneTransitionAnimation(this, searchMenuView, + getString(R.string.transition_search_back)).toBundle(); + startActivityForResult(new Intent(this, SearchActivity.class), RC_SEARCH, options); return true; case R.id.menu_dribbble_login: if (!dribbblePrefs.isLoggedIn()) { diff --git a/app/src/main/java/io/plaidapp/ui/SearchActivity.java b/app/src/main/java/io/plaidapp/ui/SearchActivity.java index b38dd47cb..f240a3b91 100644 --- a/app/src/main/java/io/plaidapp/ui/SearchActivity.java +++ b/app/src/main/java/io/plaidapp/ui/SearchActivity.java @@ -16,19 +16,14 @@ 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.app.SharedElementCallback; import android.content.Intent; -import android.graphics.Color; +import android.graphics.Point; import android.graphics.Typeface; -import android.graphics.drawable.AnimatedVectorDrawable; import android.os.Bundle; -import android.support.v4.content.ContextCompat; +import android.support.annotation.TransitionRes; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.InputType; @@ -39,17 +34,17 @@ import android.transition.Transition; import android.transition.TransitionInflater; import android.transition.TransitionManager; -import android.util.TypedValue; +import android.transition.TransitionSet; +import android.util.SparseArray; import android.view.View; -import android.view.ViewAnimationUtils; import android.view.ViewGroup; import android.view.ViewStub; -import android.view.ViewTreeObserver; import android.view.inputmethod.EditorInfo; import android.widget.CheckBox; import android.widget.ImageButton; import android.widget.ProgressBar; import android.widget.SearchView; +import android.widget.TextView; import java.util.List; @@ -64,15 +59,12 @@ import io.plaidapp.data.pocket.PocketUtils; import io.plaidapp.ui.recyclerview.InfiniteScrollListener; import io.plaidapp.ui.recyclerview.SlideInItemAnimator; -import io.plaidapp.ui.widget.BaselineGridTextView; -import io.plaidapp.util.AnimUtils; +import io.plaidapp.ui.transitions.CircularReveal; import io.plaidapp.util.ImeUtils; -import io.plaidapp.util.ViewUtils; +import io.plaidapp.util.TransitionUtils; 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"; @@ -93,23 +85,12 @@ public class SearchActivity extends Activity { @BindView(R.id.save_designer_news) CheckBox saveDesignerNews; @BindView(R.id.scrim) View scrim; @BindView(R.id.results_scrim) View resultsScrim; - private BaselineGridTextView noResults; + private TextView 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 SparseArray transitions = new SparseArray<>(); private SearchDataManager dataManager; private FeedAdapter adapter; - private boolean dismissing; - - 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) { @@ -117,33 +98,22 @@ protected void onCreate(Bundle 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 data) { if (data != null && data.size() > 0) { if (results.getVisibility() != View.VISIBLE) { - TransitionManager.beginDelayedTransition(container, auto); + TransitionManager.beginDelayedTransition(container, + getTransition(R.transition.search_show_results)); 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(AnimUtils.getLinearOutSlowInInterpolator - (SearchActivity - .this)); } adapter.addAndResort(data); } else { - TransitionManager.beginDelayedTransition(container, auto); + TransitionManager.beginDelayedTransition( + container, getTransition(R.transition.auto)); progress.setVisibility(View.GONE); setNoResultsVisibility(View.VISIBLE); } @@ -169,79 +139,7 @@ public void onLoadMore() { }); results.setHasFixedSize(true); - // 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(AnimUtils.getFastOutSlowInInterpolator(this)); - // 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)); - } - }, 600L); - - // fade in the other search chrome - searchBackground.animate() - .alpha(1f) - .setDuration(300L) - .setInterpolator(AnimUtils.getLinearOutSlowInInterpolator(this)); - searchView.animate() - .alpha(1f) - .setStartDelay(400L) - .setDuration(400L) - .setInterpolator(AnimUtils.getLinearOutSlowInInterpolator(this)) - .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(AnimUtils.getLinearOutSlowInInterpolator(SearchActivity - .this)); - showScrim.start(); - return false; - } - }); + setupTransitions(); onNewIntent(getIntent()); } @@ -280,151 +178,19 @@ protected void onDestroy() { @OnClick({ R.id.scrim, R.id.searchback }) protected void dismiss() { - if (dismissing) return; - dismissing = true; - - // translate the icon to match position in the launching activity - searchBackContainer.animate() - .translationX(searchBackDistanceX) - .setDuration(600L) - .setInterpolator(AnimUtils.getFastOutSlowInInterpolator(this)) - .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(AnimUtils.getFastOutLinearInInterpolator(this)) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - // prevent clicks while other anims are finishing - searchView.setVisibility(View.INVISIBLE); - } - }) - .start(); - searchBackground.animate() - .alpha(0f) - .setStartDelay(300L) - .setDuration(160L) - .setInterpolator(AnimUtils.getFastOutLinearInInterpolator(this)) - .setListener(null) - .start(); - if (searchToolbar.getZ() != 0f) { - searchToolbar.animate() - .z(0f) - .setDuration(600L) - .setInterpolator(AnimUtils.getFastOutLinearInInterpolator(this)) - .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(AnimUtils.getFastOutSlowInInterpolator(SearchActivity - .this)); - 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(AnimUtils.getFastOutLinearInInterpolator(this)) - .setListener(null) - .start(); + finishAfterTransition(); } @OnClick(R.id.fab) protected void save() { // show the save confirmation bubble + TransitionManager.beginDelayedTransition( + resultsContainer, getTransition(R.transition.search_show_confirm)); 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(AnimUtils.getFastOutSlowInInterpolator(SearchActivity.this)); - 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(AnimUtils.getLinearOutSlowInInterpolator(SearchActivity - .this)); - revealScrim.start(); - ObjectAnimator fadeInScrim = ObjectAnimator.ofArgb(resultsScrim, - ViewUtils.BACKGROUND_COLOR, - Color.TRANSPARENT, - ContextCompat.getColor(SearchActivity.this, R.color.scrim)); - fadeInScrim.setDuration(800L); - fadeInScrim.setInterpolator(AnimUtils.getLinearOutSlowInInterpolator(SearchActivity - .this)); - 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(AnimUtils.getLinearOutSlowInInterpolator(SearchActivity - .this)); - saveDesignerNews.setAlpha(0.6f); - saveDesignerNews.setTranslationY(saveDesignerNews.getHeight() * 0.5f); - saveDesignerNews.animate() - .alpha(1f) - .translationY(0f) - .setDuration(200L) - .setInterpolator(AnimUtils.getLinearOutSlowInInterpolator(SearchActivity - .this)); - return false; - } - }); } @OnClick(R.id.save_confirmed) @@ -440,29 +206,11 @@ protected void doSave() { @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(AnimUtils.getFastOutSlowInInterpolator - (SearchActivity.this)); - hideConfirmation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - confirmSaveContainer.setVisibility(View.GONE); - resultsScrim.setVisibility(View.GONE); - fab.setVisibility(results.getVisibility()); - } - }); - hideConfirmation.start(); + TransitionManager.beginDelayedTransition( + resultsContainer, getTransition(R.transition.search_hide_confirm)); + confirmSaveContainer.setVisibility(View.GONE); + resultsScrim.setVisibility(View.GONE); + fab.setVisibility(results.getVisibility()); } } @@ -499,10 +247,43 @@ public void onFocusChange(View v, boolean hasFocus) { }); } + private void setupTransitions() { + // grab the position that the search icon transitions in *from* + // & use it to configure the return transition + setEnterSharedElementCallback(new SharedElementCallback() { + @Override + public void onSharedElementStart( + List sharedElementNames, + List sharedElements, + List sharedElementSnapshots) { + if (sharedElements != null && !sharedElements.isEmpty()) { + View searchIcon = sharedElements.get(0); + if (searchIcon.getId() != R.id.searchback) return; + int centerX = (searchIcon.getLeft() + searchIcon.getRight()) / 2; + CircularReveal hideResults = (CircularReveal) TransitionUtils.findTransition( + (TransitionSet) getWindow().getReturnTransition(), + CircularReveal.class, R.id.results_container); + if (hideResults != null) { + hideResults.setCenter(new Point(centerX, 0)); + } + } + } + }); + // focus the search view once the transition finishes + getWindow().getEnterTransition().addListener( + new TransitionUtils.TransitionListenerAdapter() { + @Override + public void onTransitionEnd(Transition transition) { + searchView.requestFocus(); + ImeUtils.showIme(searchView); + } + }); + } + private void clearResults() { + TransitionManager.beginDelayedTransition(container, getTransition(R.transition.auto)); adapter.clear(); dataManager.clear(); - TransitionManager.beginDelayedTransition(container, auto); results.setVisibility(View.GONE); progress.setVisibility(View.GONE); fab.setVisibility(View.GONE); @@ -514,7 +295,7 @@ private void clearResults() { private void setNoResultsVisibility(int visibility) { if (visibility == View.VISIBLE) { if (noResults == null) { - noResults = (BaselineGridTextView) ((ViewStub) + noResults = (TextView) ((ViewStub) findViewById(R.id.stub_no_search_results)).inflate(); noResults.setOnClickListener(new View.OnClickListener() { @Override @@ -525,8 +306,8 @@ public void onClick(View v) { } }); } - String message = String.format(getString(R - .string.no_search_results), searchView.getQuery().toString()); + 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, @@ -546,4 +327,13 @@ private void searchFor(String query) { searchView.clearFocus(); dataManager.searchFor(query); } + + private Transition getTransition(@TransitionRes int transitionId) { + Transition transition = transitions.get(transitionId); + if (transition == null) { + transition = TransitionInflater.from(this).inflateTransition(transitionId); + transitions.put(transitionId, transition); + } + return transition; + } } diff --git a/app/src/main/java/io/plaidapp/ui/transitions/CircularReveal.java b/app/src/main/java/io/plaidapp/ui/transitions/CircularReveal.java new file mode 100644 index 000000000..d28165dbf --- /dev/null +++ b/app/src/main/java/io/plaidapp/ui/transitions/CircularReveal.java @@ -0,0 +1,148 @@ +/* + * Copyright 2016 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.transitions; + +import android.animation.Animator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Point; +import android.support.annotation.IdRes; +import android.support.annotation.NonNull; +import android.transition.TransitionValues; +import android.transition.Visibility; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewAnimationUtils; +import android.view.ViewGroup; + +import io.plaidapp.R; +import io.plaidapp.util.AnimUtils; + +/** + * A transition which shows/hides a view with a circular clipping mask. Callers should provide the + * center point of the reveal either {@link #setCenter(Point) directly} or by + * {@link #centerOn(View) specifying} another view to center on; otherwise the target {@code view}'s + * pivot point will be used. + */ +public class CircularReveal extends Visibility { + + private Point center; + private float startRadius; + private float endRadius; + private @IdRes int centerOnId = View.NO_ID; + private View centerOn; + + public CircularReveal() { + super(); + } + + public CircularReveal(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircularReveal); + startRadius = a.getDimension(R.styleable.CircularReveal_startRadius, 0f); + endRadius = a.getDimension(R.styleable.CircularReveal_endRadius, 0f); + centerOnId = a.getResourceId(R.styleable.CircularReveal_centerOn, View.NO_ID); + a.recycle(); + } + + /** + * The center point of the reveal or conceal, relative to the target {@code view}. + */ + public void setCenter(@NonNull Point center) { + this.center = center; + } + + /** + * Center the reveal or conceal on this view. + */ + public void centerOn(@NonNull View source) { + centerOn = source; + } + + /** + * Sets the radius that reveals start from. + */ + public void setStartRadius(float startRadius) { + this.startRadius = startRadius; + } + + /** + * Sets the radius that conceals end at. + */ + public void setEndRadius(float endRadius) { + this.endRadius = endRadius; + } + + @Override + public Animator onAppear(ViewGroup sceneRoot, View view, + TransitionValues startValues, + TransitionValues endValues) { + if (view == null || view.getHeight() == 0 || view.getWidth() == 0) return null; + ensureCenterPoint(sceneRoot, view); + return new AnimUtils.NoPauseAnimator(ViewAnimationUtils.createCircularReveal( + view, + center.x, + center.y, + startRadius, + getFullyRevealedRadius(view))); + } + + @Override + public Animator onDisappear(ViewGroup sceneRoot, View view, + TransitionValues startValues, + TransitionValues endValues) { + if (view == null || view.getHeight() == 0 || view.getWidth() == 0) return null; + ensureCenterPoint(sceneRoot, view); + return new AnimUtils.NoPauseAnimator(ViewAnimationUtils.createCircularReveal( + view, + center.x, + center.y, + getFullyRevealedRadius(view), + endRadius)); + } + + private void ensureCenterPoint(ViewGroup sceneRoot, View view) { + if (center != null) return; + if (centerOn != null || centerOnId != View.NO_ID) { + View source; + if (centerOn != null) { + source = centerOn; + } else { + source = sceneRoot.findViewById(centerOnId); + } + if (source != null) { + // use window location to allow views in diff hierarchies + int[] loc = new int[2]; + source.getLocationInWindow(loc); + int srcX = loc[0] + (source.getWidth() / 2); + int srcY = loc[1] + (source.getHeight() / 2); + view.getLocationInWindow(loc); + center = new Point(srcX - loc[0], srcY - loc[1]); + } + } + // else use the pivot point + if (center == null) { + center = new Point(Math.round(view.getPivotX()), Math.round(view.getPivotY())); + } + } + + private float getFullyRevealedRadius(@NonNull View view) { + return (float) Math.hypot( + Math.max(center.x, view.getWidth() - center.x), + Math.max(center.y, view.getHeight() - center.y)); + } +} diff --git a/app/src/main/java/io/plaidapp/ui/transitions/StartAnimatable.java b/app/src/main/java/io/plaidapp/ui/transitions/StartAnimatable.java new file mode 100644 index 000000000..887125e4b --- /dev/null +++ b/app/src/main/java/io/plaidapp/ui/transitions/StartAnimatable.java @@ -0,0 +1,92 @@ +/* + * Copyright 2016 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.transitions; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.transition.Transition; +import android.transition.TransitionValues; +import android.util.AttributeSet; +import android.view.ViewGroup; +import android.widget.ImageView; + +import io.plaidapp.R; + +/** + * A transition which sets a specified {@link Animatable} {@code drawable} on a target + * {@link ImageView} and {@link Animatable#start() starts} it when the transition begins. + */ +public class StartAnimatable extends Transition { + + private final Animatable animatable; + + public StartAnimatable(Animatable animatable) { + super(); + if (!(animatable instanceof Drawable)) { + throw new IllegalArgumentException("Non-Drawable resource provided."); + } + this.animatable = animatable; + } + + public StartAnimatable(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StartAnimatable); + Drawable drawable = a.getDrawable(R.styleable.StartAnimatable_android_src); + a.recycle(); + if (drawable instanceof Animatable) { + animatable = ((Animatable) drawable); + } else { + throw new IllegalArgumentException("Non-Animatable resource provided."); + } + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + // no-op + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + // no-op + } + + @Override + public Animator createAnimator(ViewGroup sceneRoot, + TransitionValues startValues, + TransitionValues endValues) { + if (animatable == null || endValues == null + || !(endValues.view instanceof ImageView)) return null; + + ImageView iv = ((ImageView) endValues.view); + iv.setImageDrawable((Drawable) animatable); + + // need to return a non-null Animator even though we just want to listen for the start + ValueAnimator transition = ValueAnimator.ofInt(0, 1); + transition.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + animatable.start(); + } + }); + return transition; + } +} diff --git a/app/src/main/java/io/plaidapp/util/AnimUtils.java b/app/src/main/java/io/plaidapp/util/AnimUtils.java index f5806c3b5..28c7887b6 100644 --- a/app/src/main/java/io/plaidapp/util/AnimUtils.java +++ b/app/src/main/java/io/plaidapp/util/AnimUtils.java @@ -288,32 +288,4 @@ public void onAnimationRepeat(Animator animator) { } } - public static class TransitionListenerAdapter implements Transition.TransitionListener { - - @Override - public void onTransitionStart(Transition transition) { - - } - - @Override - public void onTransitionEnd(Transition transition) { - - } - - @Override - public void onTransitionCancel(Transition transition) { - - } - - @Override - public void onTransitionPause(Transition transition) { - - } - - @Override - public void onTransitionResume(Transition transition) { - - } - } - } diff --git a/app/src/main/java/io/plaidapp/util/TransitionUtils.java b/app/src/main/java/io/plaidapp/util/TransitionUtils.java new file mode 100644 index 000000000..396687a2b --- /dev/null +++ b/app/src/main/java/io/plaidapp/util/TransitionUtils.java @@ -0,0 +1,93 @@ +/* + * Copyright 2016 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.util; + +import android.support.annotation.IdRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.transition.Transition; +import android.transition.TransitionSet; + +/** + * Utility methods for working with transitions + */ +public class TransitionUtils { + + private TransitionUtils() { } + + public static @Nullable Transition findTransition( + @NonNull TransitionSet set, @NonNull Class clazz) { + for (int i = 0; i < set.getTransitionCount(); i++) { + Transition transition = set.getTransitionAt(i); + if (transition.getClass() == clazz) { + return transition; + } + if (transition instanceof TransitionSet) { + Transition child = findTransition((TransitionSet) transition, clazz); + if (child != null) return child; + } + } + return null; + } + + public static @Nullable Transition findTransition( + @NonNull TransitionSet set, + @NonNull Class clazz, + @IdRes int targetId) { + for (int i = 0; i < set.getTransitionCount(); i++) { + Transition transition = set.getTransitionAt(i); + if (transition.getClass() == clazz) { + if (transition.getTargetIds().contains(targetId)) { + return transition; + } + } + if (transition instanceof TransitionSet) { + Transition child = findTransition((TransitionSet) transition, clazz, targetId); + if (child != null) return child; + } + } + return null; + } + + public static class TransitionListenerAdapter implements Transition.TransitionListener { + + @Override + public void onTransitionStart(Transition transition) { + + } + + @Override + public void onTransitionEnd(Transition transition) { + + } + + @Override + public void onTransitionCancel(Transition transition) { + + } + + @Override + public void onTransitionPause(Transition transition) { + + } + + @Override + public void onTransitionResume(Transition transition) { + + } + } +} diff --git a/app/src/main/res/drawable/ic_arrow_back_padded.xml b/app/src/main/res/drawable/ic_arrow_back_padded.xml index 471fe89b5..a425885b7 100644 --- a/app/src/main/res/drawable/ic_arrow_back_padded.xml +++ b/app/src/main/res/drawable/ic_arrow_back_padded.xml @@ -15,14 +15,15 @@ limitations under the License. --> - + + android:fillColor="@android:color/white" /> diff --git a/app/src/main/res/drawable/searchback_back.xml b/app/src/main/res/drawable/searchback_back.xml index acf738b82..1d76372ad 100644 --- a/app/src/main/res/drawable/searchback_back.xml +++ b/app/src/main/res/drawable/searchback_back.xml @@ -15,11 +15,12 @@ limitations under the License. --> - + - + + android:background="@color/scrim" /> + android:iconifiedByDefault="false" + android:transitionGroup="true" /> + android:src="@drawable/ic_arrow_back_padded" + android:transitionName="@string/transition_search_back" /> @@ -154,7 +155,7 @@ android:paddingBottom="@dimen/padding_normal" android:checked="true" android:text="@string/confirm_save_dribbble_search" - android:textAppearance="@android:style/TextAppearance.Material.Body2" /> + android:textAppearance="@android:style/TextAppearance.Material.Body2" /> + android:duration="300" + android:interpolator="@android:interpolator/fast_out_slow_in" /> diff --git a/app/src/main/res/transition/search_enter.xml b/app/src/main/res/transition/search_enter.xml index 0531eaa27..2b7a4387e 100644 --- a/app/src/main/res/transition/search_enter.xml +++ b/app/src/main/res/transition/search_enter.xml @@ -15,13 +15,45 @@ limitations under the License. --> - + xmlns:app="http://schemas.android.com/apk/res-auto"> - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - diff --git a/app/src/main/res/transition/search_hide_confirm.xml b/app/src/main/res/transition/search_hide_confirm.xml new file mode 100644 index 000000000..26916ae56 --- /dev/null +++ b/app/src/main/res/transition/search_hide_confirm.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/transition/search_return.xml b/app/src/main/res/transition/search_return.xml new file mode 100644 index 000000000..5b0bcc88d --- /dev/null +++ b/app/src/main/res/transition/search_return.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/transition/search_shared_enter.xml b/app/src/main/res/transition/search_shared_enter.xml new file mode 100644 index 000000000..69c4cf369 --- /dev/null +++ b/app/src/main/res/transition/search_shared_enter.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/transition/search_shared_return.xml b/app/src/main/res/transition/search_shared_return.xml new file mode 100644 index 000000000..62333865e --- /dev/null +++ b/app/src/main/res/transition/search_shared_return.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/transition/search_show_confirm.xml b/app/src/main/res/transition/search_show_confirm.xml new file mode 100644 index 000000000..c9ab37276 --- /dev/null +++ b/app/src/main/res/transition/search_show_confirm.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/transition/search_show_results.xml b/app/src/main/res/transition/search_show_results.xml new file mode 100644 index 000000000..223214a34 --- /dev/null +++ b/app/src/main/res/transition/search_show_results.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/attrs_circular_reveal.xml b/app/src/main/res/values/attrs_circular_reveal.xml new file mode 100644 index 000000000..724953df9 --- /dev/null +++ b/app/src/main/res/values/attrs_circular_reveal.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/values/attrs_start_animatable.xml b/app/src/main/res/values/attrs_start_animatable.xml new file mode 100644 index 000000000..6e73b1446 --- /dev/null +++ b/app/src/main/res/values/attrs_start_animatable.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 002f0f728..4be99f54c 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -25,6 +25,7 @@ 48dp 64dp 56dp + 28dp 72dp 112sp 1px diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b7a023557..d682ea185 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -150,6 +150,7 @@ new_designer_news_post transition_player_avatar transition_player_background + transition_search_back io.plaidapp.shareprovider diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index db2b56fc6..cef661775 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -59,6 +59,10 @@ @color/background_super_dark @color/background_super_dark @transition/search_enter + @transition/search_return + @transition/search_shared_enter + @transition/search_shared_return + true @array/loading_placeholders_dark @color/gif_badge_dark_image