├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── scopes │ └── scope_settings.xml └── vcs.xml ├── README.md ├── Sandbox.iml ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── toddway │ │ └── sandbox │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── toddway │ │ └── sandbox │ │ ├── BaseActivity.java │ │ ├── BaseRecyclerAdapter.java │ │ ├── BitmapUtil.java │ │ ├── Navigator.java │ │ ├── OverScrollView.java │ │ ├── OverlayFragment.java │ │ ├── Thing.java │ │ ├── ThingDetailFragment.java │ │ ├── ThingListFragment.java │ │ ├── ThingRecyclerAdapter.java │ │ └── TransitionHelper.java │ └── res │ ├── anim │ ├── scale_down.xml │ ├── scale_up.xml │ ├── slide_down.xml │ ├── slide_in_right.xml │ ├── slide_out_right.xml │ └── slide_up.xml │ ├── drawable │ ├── circle.xml │ ├── circle_button.xml │ └── circle_shadow.xml │ ├── layout │ ├── activity_base.xml │ ├── fragment_overaly.xml │ ├── fragment_thing_detail.xml │ ├── fragment_thing_list.xml │ ├── list_item.xml │ └── toolbar_container.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-v21 │ └── style.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── color.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img └── activity-transitions.gif └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Sandbox -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Android Lint 39 | 40 | 41 | Class structure 42 | 43 | 44 | Data flow issues 45 | 46 | 47 | Declaration redundancy 48 | 49 | 50 | Dependency issues 51 | 52 | 53 | Imports 54 | 55 | 56 | Modularization issues 57 | 58 | 59 | Packaging issues 60 | 61 | 62 | Pattern Validation 63 | 64 | 65 | 66 | 67 | Abstraction issues 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## See [ListOfThings](https://github.com/toddway/ListOfThings) for a newer implementation. 2 | 3 | # Android Material Transitions 4 | 5 | 6 | 7 | 8 | [This Android project](https://github.com/toddway/MaterialTransitions) samples some Material Design-ish transitions for list items and floating action buttons. It uses the 9 | [the shared element concept introduced in Android 5.0](https://developer.android.com/training/material/animations.html). I tried to pull it off with pure fragment transitions and ran into a few stags (see below) 10 | so my current solution uses an activity transition for each step. 11 | 12 | [[MORE]] 13 | 14 | ## Activity Transition tricks: 15 | 16 | - Generate a background bitmap immediately before the transition and pass it to the called activity 17 | - Suppress the view overaly (used by default for activity transitions) to keep shared elements behind the toolbar & system bars 18 | - Fall back to fade and scale activity transitions when < 5.0 19 | 20 | ## Fragment Transition issues: 21 | 22 | - animating a shared element appears to only work when using .replace() - not .add() 23 | - Unlike Activity transitions, the view overlay is not used for fragment transitions so shared elements 24 | might animate behind other views (especially when reversing a transition). setElevation() helps some of the time. 25 | - if transitionName is set at runtime (with java - not xml) it may not survive all lifecycle events. 26 | E.g. when returning to a fragment from popBackStack() 27 | - On Activity transitions the second activity's elements are animated when the transition is played forward AND 28 | when reversed. For Fragment transitions, the second activity's elements are animated when played forward but 29 | the first activity's elements are animated when reversed. 30 | 31 | ## More Help 32 | - http://www.androiddesignpatterns.com/2015/01/activity-fragment-shared-element-transitions-in-depth-part3a.html 33 | - [https://android.googlesource.com/platform/frameworks/base/...](https://android.googlesource.com/platform/frameworks/base/+/a712e8cc2f16ac32ee5f1bbf5b962969f2f3451e/core/java/android/app/EnterTransitionCoordinator.java) 34 | - http://stackoverflow.com/questions/28386397/shared-element-transitions-between-views-not-activities-or-fragments 35 | - https://github.com/saulmm/Android-Material-Examples 36 | - http://stackoverflow.com/questions/29145031/shared-element-transition-works-with-fragmenttransaction-replace-but-doesnt-w 37 | 38 | License 39 | ------- 40 | 41 | Copyright 2015 Todd Way 42 | 43 | Licensed under the Apache License, Version 2.0 (the "License"); 44 | you may not use this file except in compliance with the License. 45 | You may obtain a copy of the License at 46 | 47 | http://www.apache.org/licenses/LICENSE-2.0 48 | 49 | Unless required by applicable law or agreed to in writing, software 50 | distributed under the License is distributed on an "AS IS" BASIS, 51 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 52 | See the License for the specific language governing permissions and 53 | limitations under the License. 54 | -------------------------------------------------------------------------------- /Sandbox.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "21.1.2" 6 | 7 | defaultConfig { 8 | applicationId "com.toddway.sandbox" 9 | minSdkVersion 19 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName "1.0" 13 | buildConfigField "String", "BUILD_TIME", "\"${buildTime()}\"" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | compile fileTree(dir: 'libs', include: ['*.jar']) 25 | compile 'com.android.support:appcompat-v7:22.0.0' 26 | compile 'com.android.support:support-v13:22.+' 27 | compile 'com.android.support:recyclerview-v7:22.0.+' 28 | compile 'com.android.support:cardview-v7:22.0.+' 29 | compile 'com.jakewharton:butterknife:5.1.2' 30 | compile 'com.balysv.materialmenu:material-menu-toolbar:1.5.1' 31 | } 32 | 33 | 34 | import java.text.SimpleDateFormat 35 | def buildTime() { 36 | def df = new SimpleDateFormat("M/d/yy hh:mm a") 37 | return df.format(new Date()) 38 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/toddway/sandbox/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.toddway.sandbox; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/toddway/sandbox/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.toddway.sandbox; 2 | 3 | import android.app.Activity; 4 | import android.app.Fragment; 5 | import android.app.FragmentTransaction; 6 | import android.graphics.drawable.BitmapDrawable; 7 | import android.os.Bundle; 8 | import android.support.v4.app.ActivityCompat; 9 | import android.support.v4.widget.DrawerLayout; 10 | import android.support.v7.widget.Toolbar; 11 | import android.view.Menu; 12 | import android.view.MenuItem; 13 | import android.view.View; 14 | import android.widget.Button; 15 | import android.widget.TextView; 16 | 17 | import com.balysv.materialmenu.MaterialMenuDrawable; 18 | import com.balysv.materialmenu.MaterialMenuView; 19 | 20 | import butterknife.ButterKnife; 21 | import butterknife.InjectView; 22 | 23 | public class BaseActivity extends TransitionHelper.BaseActivity { 24 | protected static String BASE_FRAGMENT = "base_fragment"; 25 | public @InjectView(R.id.toolbar) Toolbar toolbar; 26 | public @InjectView(R.id.material_menu_button) MaterialMenuView homeButton; 27 | public @InjectView(R.id.toolbar_title) TextView toolbarTitle; 28 | public @InjectView(R.id.fab) Button fab; 29 | public @InjectView(R.id.drawerLayout) DrawerLayout drawerLayout; 30 | public @InjectView(R.id.base_fragment_background) View fragmentBackround; 31 | 32 | 33 | @Override 34 | protected void onCreate(Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | setContentView(getLayoutResource()); 37 | ButterKnife.inject(this); 38 | initToolbar(); 39 | initBaseFragment(savedInstanceState); 40 | } 41 | 42 | private void initToolbar() { 43 | if (toolbar != null) { 44 | setSupportActionBar(toolbar); 45 | getSupportActionBar().setDisplayHomeAsUpEnabled(false); 46 | getSupportActionBar().setTitle(""); 47 | homeButton.setOnClickListener(new View.OnClickListener() { 48 | @Override 49 | public void onClick(View v) { 50 | onBackPressed(); 51 | } 52 | }); 53 | } 54 | } 55 | 56 | private void initBaseFragment(Bundle savedInstanceState) { 57 | //apply background bitmap if we have one 58 | if (getIntent().hasExtra("bitmap_id")) { 59 | fragmentBackround.setBackground(new BitmapDrawable(getResources(), BitmapUtil.fetchBitmapFromIntent(getIntent()))); 60 | } 61 | 62 | Fragment fragment = null; 63 | if (savedInstanceState != null) { 64 | fragment = getFragmentManager().findFragmentByTag(BASE_FRAGMENT); 65 | } 66 | if (fragment == null) fragment = getBaseFragment(); 67 | setBaseFragment(fragment); 68 | } 69 | 70 | protected int getLayoutResource() { 71 | return R.layout.activity_base; 72 | }; 73 | 74 | protected Fragment getBaseFragment() { 75 | int fragmentResourceId = getIntent().getIntExtra("fragment_resource_id", R.layout.fragment_thing_list); 76 | switch (fragmentResourceId) { 77 | case R.layout.fragment_thing_list: 78 | default: 79 | return new ThingListFragment(); 80 | case R.layout.fragment_thing_detail: 81 | return ThingDetailFragment.create(); 82 | case R.layout.fragment_overaly: 83 | return new OverlayFragment(); 84 | } 85 | } 86 | 87 | public void setBaseFragment(Fragment fragment) { 88 | if (fragment == null) return; 89 | FragmentTransaction transaction = getFragmentManager().beginTransaction(); 90 | transaction.replace(R.id.base_fragment, fragment, BASE_FRAGMENT); 91 | transaction.commit(); 92 | } 93 | 94 | 95 | private MaterialMenuDrawable.IconState currentIconState; 96 | public boolean animateHomeIcon(MaterialMenuDrawable.IconState iconState) { 97 | if (currentIconState == iconState) return false; 98 | currentIconState = iconState; 99 | homeButton.animateState(currentIconState); 100 | return true; 101 | } 102 | 103 | public void setHomeIcon(MaterialMenuDrawable.IconState iconState) { 104 | if (currentIconState == iconState) return; 105 | currentIconState = iconState; 106 | homeButton.setState(currentIconState); 107 | 108 | } 109 | 110 | @Override 111 | public boolean onCreateOptionsMenu(Menu menu) { 112 | // Inflate the menu; this adds items to the action bar if it is present. 113 | getMenuInflater().inflate(R.menu.menu_main, menu); 114 | return true; 115 | } 116 | 117 | @Override 118 | public boolean onOptionsItemSelected(MenuItem item) { 119 | int id = item.getItemId(); 120 | 121 | if (id == R.id.action_settings) { 122 | return true; 123 | } 124 | 125 | return super.onOptionsItemSelected(item); 126 | } 127 | 128 | 129 | @Override 130 | public boolean onBeforeBack() { 131 | ActivityCompat.finishAfterTransition(this); 132 | return false; 133 | } 134 | 135 | public static BaseActivity of(Activity activity) { 136 | return (BaseActivity) activity; 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /app/src/main/java/com/toddway/sandbox/BaseRecyclerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.toddway.sandbox; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.util.SparseBooleanArray; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | import butterknife.ButterKnife; 14 | 15 | 16 | public class BaseRecyclerAdapter extends RecyclerView.Adapter.ViewHolder> { 17 | private List items = Collections.emptyList(); 18 | private SparseBooleanArray selectedItems; 19 | private OnItemClickListener onItemClickListener; 20 | 21 | 22 | @Override 23 | public BaseRecyclerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 24 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent, false); 25 | return new ViewHolder(view); 26 | } 27 | 28 | @Override 29 | public void onBindViewHolder(BaseRecyclerAdapter.ViewHolder holder, int position) { 30 | holder.populate(items.get(position)); 31 | } 32 | 33 | @Override 34 | public int getItemCount() { 35 | return items.size(); 36 | } 37 | 38 | public void updateList(List thingList) { 39 | this.items = thingList; 40 | notifyDataSetChanged(); 41 | } 42 | 43 | public void toggleSelection(int pos) { 44 | if (selectedItems.get(pos, false)) { 45 | selectedItems.delete(pos); 46 | } 47 | else { 48 | selectedItems.put(pos, true); 49 | } 50 | notifyItemChanged(pos); 51 | } 52 | 53 | public void clearSelections() { 54 | selectedItems.clear(); 55 | notifyDataSetChanged(); 56 | } 57 | 58 | public int getSelectedItemCount() { 59 | return selectedItems.size(); 60 | } 61 | 62 | public List getSelectedItems() { 63 | List items = new ArrayList(selectedItems.size()); 64 | for (int i = 0; i < selectedItems.size(); i++) { 65 | items.add(selectedItems.keyAt(i)); 66 | } 67 | return items; 68 | } 69 | 70 | public static interface OnItemClickListener { 71 | public void onItemClick(View view, T item, boolean isLongClick); 72 | } 73 | 74 | public void setOnItemClickListener(final OnItemClickListener onItemClickListener) { 75 | this.onItemClickListener = onItemClickListener; 76 | } 77 | 78 | 79 | public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener{ 80 | 81 | public ViewHolder(View itemView) { 82 | super(itemView); 83 | ButterKnife.inject(this, itemView); 84 | itemView.setOnClickListener(this); 85 | itemView.setOnLongClickListener(this); 86 | } 87 | 88 | public void populate(T item) { 89 | 90 | } 91 | 92 | @Override 93 | public void onClick(View v) { 94 | handleClick(v, false); 95 | } 96 | 97 | @Override 98 | public boolean onLongClick(View v) { 99 | return handleClick(v, true); 100 | } 101 | 102 | private boolean handleClick(View v, boolean isLongClick) { 103 | if (onItemClickListener != null) { 104 | onItemClickListener.onItemClick(v, items.get(getAdapterPosition()), isLongClick); 105 | return true; 106 | } 107 | return false; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/com/toddway/sandbox/BitmapUtil.java: -------------------------------------------------------------------------------- 1 | package com.toddway.sandbox; 2 | 3 | import android.content.Intent; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Canvas; 6 | import android.util.LruCache; 7 | import android.view.View; 8 | 9 | import java.util.UUID; 10 | 11 | 12 | public class BitmapUtil { 13 | private static LruCache mMemoryCache; 14 | static { 15 | final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); 16 | final int cacheSize = maxMemory / 8; // Use 1/8th of the available memory for this memory cache. 17 | mMemoryCache = new LruCache(cacheSize) { 18 | @Override 19 | protected int sizeOf(String key, Bitmap bitmap) { 20 | return bitmap.getByteCount() / 1024; // The cache size will be measured in kilobytes rather than number of items. 21 | } 22 | }; 23 | } 24 | 25 | public static void storeBitmapInIntent(Bitmap bitmap, Intent intent) { 26 | String key = "bitmap_" + UUID.randomUUID(); 27 | storeBitmapInMemCache(key, bitmap); 28 | intent.putExtra("bitmap_id", key); 29 | 30 | // ByteArrayOutputStream bs = new ByteArrayOutputStream(); 31 | // bitmap.compress(Bitmap.CompressFormat.PNG, 0, bs); 32 | // intent.putExtra("background", bs.toByteArray()); 33 | } 34 | 35 | public static void storeBitmapInMemCache(String key, Bitmap bitmap) { 36 | if (getBitmapFromMemCache(key) == null) { 37 | mMemoryCache.put(key, bitmap); 38 | } 39 | } 40 | 41 | public static Bitmap getBitmapFromMemCache(String key) { 42 | return mMemoryCache.get(key); 43 | } 44 | 45 | public static Bitmap fetchBitmapFromIntent(Intent intent) { 46 | String key = intent.getStringExtra("bitmap_id"); 47 | // byte[] byteArray = intent.getByteArrayExtra("background"); 48 | // Bitmap bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length); 49 | return getBitmapFromMemCache(key); 50 | } 51 | 52 | public static Bitmap createBitmap(View v) { 53 | Bitmap bitmap = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888); 54 | Canvas canvas = new Canvas(bitmap); 55 | v.draw(canvas); 56 | return bitmap; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/toddway/sandbox/Navigator.java: -------------------------------------------------------------------------------- 1 | package com.toddway.sandbox; 2 | 3 | 4 | import android.content.Intent; 5 | import android.support.v4.app.ActivityCompat; 6 | import android.support.v4.app.ActivityOptionsCompat; 7 | import android.support.v4.util.Pair; 8 | import android.support.v4.view.ViewCompat; 9 | import android.view.View; 10 | 11 | public class Navigator { 12 | 13 | public static int ANIM_DURATION = 350; 14 | 15 | public static void launchDetail(BaseActivity fromActivity, View fromView, Thing item, View backgroundView) { 16 | ViewCompat.setTransitionName(fromView, "detail_element"); 17 | ViewCompat.setTransitionName(fromActivity.findViewById(R.id.fab), "fab"); 18 | ActivityOptionsCompat options = 19 | TransitionHelper.makeOptionsCompat( 20 | fromActivity, 21 | Pair.create(fromView, "detail_element"), 22 | Pair.create(fromActivity.findViewById(R.id.fab), "fab") 23 | ); 24 | Intent intent = new Intent(fromActivity, BaseActivity.class); 25 | intent.putExtra("item_text", item.text); 26 | intent.putExtra("fragment_resource_id", R.layout.fragment_thing_detail); 27 | 28 | if (backgroundView != null) BitmapUtil.storeBitmapInIntent(BitmapUtil.createBitmap(backgroundView), intent); 29 | 30 | ActivityCompat.startActivity(fromActivity, intent, options.toBundle()); 31 | 32 | fromActivity.overridePendingTransition(R.anim.slide_up, R.anim.scale_down); 33 | } 34 | 35 | public static void launchOverlay(BaseActivity fromActivity, View fromView, View backgroundView) { 36 | ActivityOptionsCompat options = 37 | TransitionHelper.makeOptionsCompat( 38 | fromActivity 39 | ); 40 | Intent intent = new Intent(fromActivity, BaseActivity.class); 41 | intent.putExtra("fragment_resource_id", R.layout.fragment_overaly); 42 | 43 | if (backgroundView != null) BitmapUtil.storeBitmapInIntent(BitmapUtil.createBitmap(backgroundView), intent); 44 | 45 | ActivityCompat.startActivity(fromActivity, intent, options.toBundle()); 46 | 47 | fromActivity.overridePendingTransition(R.anim.slide_up, R.anim.scale_down); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/toddway/sandbox/OverScrollView.java: -------------------------------------------------------------------------------- 1 | package com.toddway.sandbox; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.MotionEvent; 6 | import android.view.View; 7 | import android.view.animation.DecelerateInterpolator; 8 | import android.widget.ScrollView; 9 | 10 | public class OverScrollView extends ScrollView { 11 | 12 | public OverScrollView(Context context) { 13 | super(context); 14 | } 15 | 16 | public OverScrollView(Context context, AttributeSet attrs) { 17 | super(context, attrs); 18 | } 19 | 20 | public OverScrollView(Context context, AttributeSet attrs, int defStyleAttr) { 21 | super(context, attrs, defStyleAttr); 22 | } 23 | 24 | private int lastEventY; 25 | 26 | @Override 27 | public boolean onTouchEvent(MotionEvent event) { 28 | final int eventY = (int) event.getY(); 29 | switch (event.getAction()) { 30 | case MotionEvent.ACTION_UP: 31 | int yDistance = (int) getTranslationY(); 32 | if (yDistance != 0 && listener != null) { 33 | if (!listener.onOverScroll(yDistance, true)) { //only do this if listener returns false 34 | animate().translationY(0) 35 | .setDuration(200) 36 | .setInterpolator(new DecelerateInterpolator(6)) 37 | .start(); 38 | } 39 | } 40 | break; 41 | case MotionEvent.ACTION_DOWN: 42 | lastEventY = eventY; 43 | break; 44 | case MotionEvent.ACTION_MOVE: 45 | if (getScrollY() == 0) { 46 | handleOverscroll(event, false); 47 | } else { 48 | View view = getChildAt(getChildCount() - 1); 49 | if (view.getHeight() <= (getHeight() + getScrollY())) { 50 | handleOverscroll(event, true); 51 | } 52 | } 53 | break; 54 | } 55 | 56 | if (getTranslationY() != 0) { 57 | return true; 58 | } 59 | return super.onTouchEvent(event); 60 | 61 | } 62 | 63 | public static interface OverScrollListener { 64 | public boolean onOverScroll(int yDistance, boolean isReleased); 65 | } 66 | 67 | private OverScrollListener listener; 68 | public void setOverScrollListener(OverScrollListener listener) { 69 | this.listener = listener; 70 | } 71 | 72 | private void handleOverscroll(MotionEvent ev, boolean isBottom) { 73 | int pointerCount = ev.getHistorySize(); 74 | for (int p = 0; p < pointerCount; p++) { 75 | int historicalY = (int) ev.getHistoricalY(p); 76 | int yDistance = (historicalY - lastEventY) / 6; 77 | 78 | if ((isBottom && yDistance < 0) || (!isBottom && yDistance > 0)) { 79 | setTranslationY(yDistance); 80 | if (listener != null) listener.onOverScroll(yDistance, false); 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/toddway/sandbox/OverlayFragment.java: -------------------------------------------------------------------------------- 1 | package com.toddway.sandbox; 2 | 3 | 4 | import android.animation.Animator; 5 | import android.animation.AnimatorListenerAdapter; 6 | import android.animation.ArgbEvaluator; 7 | import android.animation.ValueAnimator; 8 | import android.os.Bundle; 9 | import android.os.Handler; 10 | import android.support.v4.view.ViewCompat; 11 | import android.view.LayoutInflater; 12 | import android.view.View; 13 | import android.view.ViewAnimationUtils; 14 | import android.view.ViewGroup; 15 | import android.view.animation.AccelerateInterpolator; 16 | import android.view.animation.DecelerateInterpolator; 17 | import android.widget.RelativeLayout; 18 | import android.widget.TextView; 19 | 20 | import com.balysv.materialmenu.MaterialMenuDrawable; 21 | 22 | import butterknife.ButterKnife; 23 | import butterknife.InjectView; 24 | 25 | public class OverlayFragment extends TransitionHelper.BaseFragment { 26 | 27 | @InjectView(R.id.overlay) RelativeLayout overlayLayout; 28 | @InjectView(R.id.text_view) TextView textView; 29 | 30 | public OverlayFragment() {} 31 | 32 | @Override 33 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 34 | View rootView = inflater.inflate(R.layout.fragment_overaly, container, false); 35 | ButterKnife.inject(this, rootView); 36 | initBodyText(); 37 | return rootView; 38 | } 39 | 40 | private void initBodyText() { 41 | textView.setText("v1.0.0"); 42 | textView.setAlpha(0); 43 | textView.setTranslationY(100); 44 | new Handler().postDelayed(new Runnable(){ 45 | public void run() { 46 | textView.animate() 47 | .alpha(1) 48 | .setStartDelay(Navigator.ANIM_DURATION/3) 49 | .setDuration(Navigator.ANIM_DURATION*5) 50 | .setInterpolator(new DecelerateInterpolator(9)) 51 | .translationY(0) 52 | .start(); 53 | } 54 | }, 200); 55 | } 56 | 57 | @Override 58 | public void onBeforeEnter(View contentView) { 59 | overlayLayout.setVisibility(View.INVISIBLE); 60 | BaseActivity.of(getActivity()).setHomeIcon(MaterialMenuDrawable.IconState.BURGER); 61 | BaseActivity.of(getActivity()).animateHomeIcon(MaterialMenuDrawable.IconState.ARROW); 62 | } 63 | 64 | @Override 65 | public void onAfterEnter() { 66 | animateRevealShow(overlayLayout); 67 | } 68 | 69 | @Override 70 | public boolean onBeforeBack() { 71 | animateRevealHide(overlayLayout); 72 | BaseActivity.of(getActivity()).animateHomeIcon(MaterialMenuDrawable.IconState.BURGER); 73 | return false; 74 | } 75 | 76 | @Override 77 | public void onBeforeViewShows(View contentView) { 78 | ViewCompat.setTransitionName(getActivity().findViewById(R.id.fab), "fab"); 79 | BaseActivity.of(getActivity()).fab.setVisibility(View.INVISIBLE); 80 | TransitionHelper.excludeEnterTarget(getActivity(), R.id.toolbar_container, true); 81 | TransitionHelper.excludeEnterTarget(getActivity(), R.id.full_screen, true); 82 | TransitionHelper.excludeEnterTarget(getActivity(), R.id.overlay, true); 83 | } 84 | 85 | // @TargetApi(21) 86 | // private void initSharedElementTransition() { 87 | // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 88 | // overlayLayout.setVisibility(View.INVISIBLE); 89 | // ViewCompat.setTransitionName(getBaseActivity().findViewById(R.id.fab), "fab"); 90 | // getActivity().getWindow().getSharedElementEnterTransition().setDuration(Navigator.ANIM_DURATION); 91 | // getActivity().getWindow().getSharedElementEnterTransition().addListener(new Transition.TransitionListener() { 92 | // @Override 93 | // public void onTransitionStart(Transition transition) { 94 | // if (overlayLayout.getVisibility() == View.INVISIBLE) { 95 | // animateRevealShow(overlayLayout); 96 | // } else { 97 | // animateRevealHide(overlayLayout); 98 | // } 99 | // } 100 | // 101 | // @Override 102 | // public void onTransitionEnd(Transition transition) { 103 | // 104 | // } 105 | // 106 | // @Override 107 | // public void onTransitionCancel(Transition transition) { 108 | // 109 | // } 110 | // 111 | // @Override 112 | // public void onTransitionPause(Transition transition) { 113 | // 114 | // } 115 | // 116 | // @Override 117 | // public void onTransitionResume(Transition transition) { 118 | // 119 | // } 120 | // }); 121 | // } 122 | // } 123 | 124 | public void animateRevealShow(View viewRoot) { 125 | View fab = BaseActivity.of(getActivity()).fab; 126 | int cx = fab.getLeft() + (fab.getWidth()/2); //middle of button 127 | int cy = fab.getTop() + (fab.getHeight()/2); //middle of button 128 | int radius = (int) Math.sqrt(Math.pow(cx, 2) + Math.pow(cy, 2)); //hypotenuse to top left 129 | 130 | Animator anim = ViewAnimationUtils.createCircularReveal(viewRoot, cx, cy, 0, radius); 131 | viewRoot.setVisibility(View.VISIBLE); 132 | anim.setInterpolator(new DecelerateInterpolator()); 133 | anim.setDuration(Navigator.ANIM_DURATION); 134 | anim.start(); 135 | 136 | 137 | } 138 | 139 | public void animateRevealHide(final View viewRoot) { 140 | View fab = BaseActivity.of(getActivity()).fab; 141 | int cx = fab.getLeft() + (fab.getWidth()/2); //middle of button 142 | int cy = fab.getTop() + (fab.getHeight()/2); //middle of button 143 | int radius = (int) Math.sqrt(Math.pow(cx, 2) + Math.pow(cy, 2)); //hypotenuse to top left 144 | 145 | Animator anim = ViewAnimationUtils.createCircularReveal(viewRoot, cx, cy, radius, 0); 146 | anim.addListener(new AnimatorListenerAdapter() { 147 | @Override 148 | public void onAnimationEnd(Animator animation) { 149 | super.onAnimationEnd(animation); 150 | viewRoot.setVisibility(View.INVISIBLE); 151 | } 152 | }); 153 | //anim.setInterpolator(new AccelerateInterpolator()); 154 | anim.setDuration(Navigator.ANIM_DURATION); 155 | anim.start(); 156 | 157 | Integer colorTo = getResources().getColor(R.color.primaryColor); 158 | Integer colorFrom = getResources().getColor(android.R.color.white); 159 | ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); 160 | colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 161 | @Override 162 | public void onAnimationUpdate(ValueAnimator animator) { 163 | overlayLayout.setBackgroundColor((Integer)animator.getAnimatedValue()); 164 | } 165 | 166 | }); 167 | colorAnimation.setInterpolator(new AccelerateInterpolator(2)); 168 | colorAnimation.setDuration(Navigator.ANIM_DURATION); 169 | colorAnimation.start(); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /app/src/main/java/com/toddway/sandbox/Thing.java: -------------------------------------------------------------------------------- 1 | package com.toddway.sandbox; 2 | 3 | public class Thing { 4 | public String text; 5 | public String color; 6 | public Thing(String text, String color) { 7 | this.text = text; 8 | this.color = color; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/toddway/sandbox/ThingDetailFragment.java: -------------------------------------------------------------------------------- 1 | package com.toddway.sandbox; 2 | 3 | 4 | import android.os.Bundle; 5 | import android.os.Handler; 6 | import android.support.v4.view.ViewCompat; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.view.animation.AccelerateInterpolator; 11 | import android.view.animation.DecelerateInterpolator; 12 | import android.widget.TextView; 13 | 14 | import com.balysv.materialmenu.MaterialMenuDrawable; 15 | 16 | import butterknife.ButterKnife; 17 | import butterknife.InjectView; 18 | 19 | public class ThingDetailFragment extends TransitionHelper.BaseFragment { 20 | 21 | @InjectView(R.id.detail_title) TextView titleTextView; 22 | @InjectView(R.id.detail_body) TextView detailBodyTextView; 23 | @InjectView(R.id.overscroll_view) OverScrollView scrollView; 24 | 25 | public static ThingDetailFragment create() { 26 | ThingDetailFragment f = new ThingDetailFragment(); 27 | return f; 28 | } 29 | 30 | public ThingDetailFragment() {} 31 | 32 | @Override 33 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 34 | View rootView = inflater.inflate(R.layout.fragment_thing_detail, container, false); 35 | ButterKnife.inject(this, rootView); 36 | String itemText = getActivity().getIntent().getStringExtra("item_text"); 37 | titleTextView.setText(itemText); 38 | 39 | scrollView.setOverScrollListener(new OverScrollView.OverScrollListener() { 40 | int translationThreshold = 100; 41 | @Override 42 | public boolean onOverScroll(int yDistance, boolean isReleased) { 43 | if (Math.abs(yDistance) > translationThreshold) { //passed threshold 44 | if (isReleased) { 45 | getActivity().onBackPressed(); 46 | return true; 47 | } else { 48 | BaseActivity.of(getActivity()).animateHomeIcon(MaterialMenuDrawable.IconState.X); 49 | } 50 | } else { 51 | BaseActivity.of(getActivity()).animateHomeIcon(MaterialMenuDrawable.IconState.ARROW); 52 | } 53 | return false; 54 | } 55 | }); 56 | 57 | initDetailBody(); 58 | return rootView; 59 | } 60 | 61 | private void initDetailBody() { 62 | detailBodyTextView.setAlpha(0); 63 | new Handler().postDelayed(new Runnable(){ 64 | public void run() { 65 | detailBodyTextView.animate().alpha(1).start(); 66 | } 67 | }, 500); 68 | } 69 | 70 | @Override 71 | public void onBeforeViewShows(View contentView) { 72 | ViewCompat.setTransitionName(scrollView, "detail_element"); 73 | ViewCompat.setTransitionName(getActivity().findViewById(R.id.fab), "fab"); 74 | BaseActivity.of(getActivity()).fab.setTranslationY(400); 75 | 76 | TransitionHelper.excludeEnterTarget(getActivity(), R.id.toolbar_container, true); 77 | TransitionHelper.excludeEnterTarget(getActivity(), R.id.full_screen, true); 78 | } 79 | 80 | @Override 81 | public void onBeforeEnter(View contentView) { 82 | BaseActivity.of(getActivity()).fragmentBackround.animate().scaleX(.92f).scaleY(.92f).alpha(.6f).setDuration(Navigator.ANIM_DURATION).setInterpolator(new AccelerateInterpolator()).start(); 83 | BaseActivity.of(getActivity()).setHomeIcon(MaterialMenuDrawable.IconState.BURGER); 84 | BaseActivity.of(getActivity()).animateHomeIcon(MaterialMenuDrawable.IconState.ARROW); 85 | } 86 | 87 | @Override 88 | public boolean onBeforeBack() { 89 | BaseActivity.of(getActivity()).animateHomeIcon(MaterialMenuDrawable.IconState.BURGER); 90 | BaseActivity.of(getActivity()).fragmentBackround.animate().scaleX(1).scaleY(1).alpha(1).translationY(0).setDuration(Navigator.ANIM_DURATION/4).setInterpolator(new DecelerateInterpolator()).start(); 91 | TransitionHelper.fadeThenFinish(detailBodyTextView, getActivity()); 92 | return false; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/com/toddway/sandbox/ThingListFragment.java: -------------------------------------------------------------------------------- 1 | package com.toddway.sandbox; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.widget.LinearLayoutManager; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.Gravity; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | 11 | import com.balysv.materialmenu.MaterialMenuDrawable; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | import butterknife.ButterKnife; 17 | import butterknife.InjectView; 18 | 19 | public class ThingListFragment extends TransitionHelper.BaseFragment { 20 | @InjectView(R.id.recycler) 21 | RecyclerView recyclerView; 22 | ThingRecyclerAdapter recyclerAdapter; 23 | 24 | public ThingListFragment() {} 25 | 26 | @Override 27 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 28 | View rootView = inflater.inflate(R.layout.fragment_thing_list, container, false); 29 | ButterKnife.inject(this, rootView); 30 | initRecyclerView(); 31 | return rootView; 32 | } 33 | 34 | private void initRecyclerView() { 35 | recyclerAdapter = new ThingRecyclerAdapter(); 36 | recyclerAdapter.updateList(getThings()); 37 | recyclerAdapter.setOnItemClickListener(new BaseRecyclerAdapter.OnItemClickListener() { 38 | @Override 39 | public void onItemClick(View view, Thing item, boolean isLongClick) { 40 | if (isLongClick) { 41 | BaseActivity.of(getActivity()).animateHomeIcon(MaterialMenuDrawable.IconState.X); 42 | } else { 43 | Navigator.launchDetail(BaseActivity.of(getActivity()), view, item, recyclerView); 44 | } 45 | } 46 | }); 47 | 48 | recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); 49 | recyclerView.setAdapter(recyclerAdapter); 50 | 51 | BaseActivity.of(getActivity()).fab.setOnClickListener(new View.OnClickListener() { 52 | @Override 53 | public void onClick(View v) { 54 | Navigator.launchOverlay(BaseActivity.of(getActivity()), v, getActivity().findViewById(R.id.base_fragment_container)); 55 | } 56 | }); 57 | } 58 | 59 | @Override 60 | public boolean onBeforeBack() { 61 | BaseActivity activity = BaseActivity.of(getActivity()); 62 | if (!activity.animateHomeIcon(MaterialMenuDrawable.IconState.BURGER)) { 63 | activity.drawerLayout.openDrawer(Gravity.START); 64 | } 65 | return super.onBeforeBack(); 66 | } 67 | 68 | public static List getThings() { 69 | ArrayList list = new ArrayList<>(); 70 | for (int l = 0; l < 100; l++) { 71 | list.add(new Thing("Thing "+l, null)); 72 | } 73 | return list; 74 | } 75 | 76 | } 77 | 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/toddway/sandbox/ThingRecyclerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.toddway.sandbox; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.TextView; 7 | 8 | import butterknife.InjectView; 9 | 10 | 11 | public class ThingRecyclerAdapter extends BaseRecyclerAdapter { 12 | 13 | @Override 14 | public ThingHolder onCreateViewHolder(ViewGroup parent, int viewType) { 15 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent, false); 16 | return new ThingHolder(view); 17 | } 18 | 19 | public class ThingHolder extends BaseRecyclerAdapter.ViewHolder { 20 | @InjectView(R.id.title) 21 | TextView titleTextView; 22 | 23 | public ThingHolder(View itemView) { 24 | super(itemView); 25 | } 26 | 27 | public void populate(Thing item) { 28 | titleTextView.setText(item.text); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/toddway/sandbox/TransitionHelper.java: -------------------------------------------------------------------------------- 1 | package com.toddway.sandbox; 2 | 3 | import android.animation.Animator; 4 | import android.app.Activity; 5 | import android.app.Fragment; 6 | import android.os.Build; 7 | import android.os.Bundle; 8 | import android.support.v4.app.ActivityCompat; 9 | import android.support.v4.app.ActivityOptionsCompat; 10 | import android.support.v4.util.Pair; 11 | import android.support.v7.app.ActionBarActivity; 12 | import android.transition.Transition; 13 | import android.view.View; 14 | import android.view.ViewTreeObserver; 15 | import android.view.Window; 16 | 17 | import java.util.ArrayList; 18 | import java.util.Arrays; 19 | import java.util.List; 20 | import java.util.ListIterator; 21 | 22 | /** 23 | * Provides extra lifecycle events and shims for shared element transitions 24 | * See the included BaseActivity and BaseFragment for example use 25 | */ 26 | public class TransitionHelper { 27 | 28 | Activity activity; 29 | 30 | private TransitionHelper(Activity activity, Bundle savedInstanceState) { 31 | this.activity = activity; 32 | isAfterEnter = savedInstanceState != null; //if saved instance is not null we've already "entered" 33 | postponeEnterTransition(); //we postpone to prevent status and nav bars from flashing during shared element transitions 34 | } 35 | 36 | /** 37 | * Should be called from Activity.onResume() 38 | */ 39 | public void onResume() { 40 | if (isAfterEnter) return; 41 | 42 | if (!isViewCreatedAlreadyCalled) onViewCreated(); 43 | 44 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 45 | onAfterEnter(); 46 | } else { 47 | activity.getWindow().getSharedElementEnterTransition().addListener(new Transition.TransitionListener() { 48 | @Override 49 | public void onTransitionStart(Transition transition) { 50 | if (isAfterEnter()) for (Listener listener : listeners) listener.onBeforeReturn(); 51 | } 52 | 53 | @Override 54 | public void onTransitionEnd(Transition transition) { 55 | if (!isAfterEnter()) onAfterEnter(); 56 | } 57 | 58 | @Override 59 | public void onTransitionCancel(Transition transition) { 60 | if (!isAfterEnter()) onAfterEnter(); 61 | } 62 | 63 | @Override 64 | public void onTransitionPause(Transition transition) { 65 | } 66 | 67 | @Override 68 | public void onTransitionResume(Transition transition) { 69 | } 70 | }); 71 | } 72 | } 73 | 74 | /** 75 | * Should be called from Activity.onBackPressed() 76 | */ 77 | public void onBackPressed() { 78 | boolean isConsumed = false; 79 | for (Listener listener : listeners) { 80 | isConsumed = listener.onBeforeBack() || isConsumed; 81 | } 82 | if (!isConsumed) ActivityCompat.finishAfterTransition(activity); 83 | } 84 | 85 | /** 86 | * Should be called immediately after all shared transition views are inflated. 87 | * If using fragments, recommend calling at the beginning of Fragment.onViewCreated(). 88 | */ 89 | private boolean isViewCreatedAlreadyCalled = false; 90 | public void onViewCreated() { 91 | if (isViewCreatedAlreadyCalled) return; 92 | isViewCreatedAlreadyCalled = true; 93 | 94 | View contentView = activity.getWindow().getDecorView().findViewById(android.R.id.content); 95 | for (Listener listener : listeners) listener.onBeforeViewShows(contentView); 96 | if (!isAfterEnter()) { for (Listener listener : listeners) listener.onBeforeEnter(contentView); } 97 | 98 | if (isPostponeEnterTransition) startPostponedEnterTransition(); 99 | } 100 | 101 | /** 102 | * Call from Activity.onSaveInstanceState() 103 | * @param outState 104 | */ 105 | public void onSaveInstanceState(Bundle outState) { 106 | outState.putBoolean("isAfterEnter", isAfterEnter); 107 | } 108 | 109 | /** 110 | * A parent object that owns an instance of TransitionHelper 111 | * Your Activity should implement Source and call TransitionHelper.init() from Activity.onCreate() 112 | */ 113 | public interface Source { 114 | /** 115 | * Getter for TransitionHelper instance 116 | * @return 117 | */ 118 | TransitionHelper getTransitionHelper(); 119 | 120 | /** 121 | * Setter for TransitionHelper instance 122 | * @param transitionHelper 123 | */ 124 | void setTransitionHelper(TransitionHelper transitionHelper); 125 | } 126 | 127 | 128 | 129 | /** 130 | * Listens for extra transition events 131 | * Activities, Fragments, and other views should implement Listener and call TransitionHelper.of(...).addListener(this) 132 | */ 133 | public interface Listener { 134 | /** 135 | * Called during every onViewCreated 136 | * @param contentView 137 | */ 138 | void onBeforeViewShows(View contentView); 139 | 140 | /** 141 | * Called during onViewCreated only on an enter transition 142 | * @param contentView 143 | */ 144 | void onBeforeEnter(View contentView); 145 | 146 | /** 147 | * Called after enter transition is finished for L+, otherwise called immediately during first onResume 148 | */ 149 | void onAfterEnter(); 150 | 151 | 152 | /** 153 | * Called during Activity.onBackPressed() 154 | * @return true if the listener has consumed the event, false otherwise 155 | */ 156 | boolean onBeforeBack(); 157 | 158 | void onBeforeReturn(); 159 | } 160 | 161 | private List listeners = new ArrayList<>(); 162 | public void addListener(Listener listener) { 163 | listeners.add(listener); 164 | } 165 | 166 | private void onAfterEnter() { 167 | for (Listener listener : listeners) listener.onAfterEnter(); 168 | isAfterEnter = true; 169 | } 170 | 171 | private boolean isAfterEnter = false; 172 | public boolean isAfterEnter() { return isAfterEnter; } 173 | 174 | private boolean isPostponeEnterTransition = false; 175 | private void postponeEnterTransition() { 176 | if (isAfterEnter) return; 177 | ActivityCompat.postponeEnterTransition(activity); 178 | isPostponeEnterTransition = true; 179 | } 180 | 181 | 182 | private void startPostponedEnterTransition() { 183 | final View decor = activity.getWindow().getDecorView(); 184 | decor.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 185 | @Override 186 | public boolean onPreDraw() { 187 | decor.getViewTreeObserver().removeOnPreDrawListener(this); 188 | ActivityCompat.startPostponedEnterTransition(activity); 189 | return true; 190 | } 191 | }); 192 | } 193 | 194 | public static void excludeEnterTarget(Activity activity, int targetId, boolean exclude) { 195 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 196 | activity.getWindow().getEnterTransition().excludeTarget(targetId, exclude); 197 | } 198 | } 199 | 200 | 201 | //STATICS: 202 | /** 203 | * Get the TransitionHelper object for an Activity 204 | * @param a 205 | * @return 206 | */ 207 | public static TransitionHelper of(Activity a) { 208 | return ((Source) a).getTransitionHelper(); 209 | } 210 | 211 | /** 212 | * Initialize the TransitionHelper object. Should be called at the beginning of Activity.onCreate() 213 | * @param source 214 | * @param savedInstanceState 215 | */ 216 | public static void init(Source source, Bundle savedInstanceState) { 217 | source.setTransitionHelper(new TransitionHelper((Activity) source, savedInstanceState)); 218 | } 219 | 220 | public static ActivityOptionsCompat makeOptionsCompat(Activity fromActivity, Pair... sharedElements) { 221 | ArrayList> list = new ArrayList<>(Arrays.asList(sharedElements)); 222 | 223 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 224 | list.add(Pair.create(fromActivity.findViewById(android.R.id.statusBarBackground), Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)); 225 | list.add(Pair.create(fromActivity.findViewById(android.R.id.navigationBarBackground), Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)); 226 | } 227 | 228 | //remove any views that are null 229 | for (ListIterator> iter = list.listIterator(); iter.hasNext();) { 230 | Pair pair = iter.next(); 231 | if (pair.first == null) iter.remove(); 232 | } 233 | 234 | sharedElements = list.toArray(new Pair[list.size()]); 235 | return ActivityOptionsCompat.makeSceneTransitionAnimation( 236 | fromActivity, 237 | sharedElements 238 | ); 239 | } 240 | 241 | public static void fadeThenFinish(View v, final Activity a) { 242 | if (v != null) { 243 | v.animate() //fade out the view before finishing the activity (for cleaner L transition) 244 | .alpha(0) 245 | .setDuration(100) 246 | .setListener( 247 | new Animator.AnimatorListener() { 248 | @Override 249 | public void onAnimationStart(Animator animation) { 250 | 251 | } 252 | 253 | @Override 254 | public void onAnimationEnd(Animator animation) { 255 | ActivityCompat.finishAfterTransition(a); 256 | } 257 | 258 | @Override 259 | public void onAnimationCancel(Animator animation) { 260 | 261 | } 262 | 263 | @Override 264 | public void onAnimationRepeat(Animator animation) { 265 | 266 | } 267 | } 268 | ) 269 | .start(); 270 | } 271 | } 272 | 273 | public static class BaseActivity extends ActionBarActivity implements TransitionHelper.Source, TransitionHelper.Listener { 274 | TransitionHelper transitionHelper; 275 | 276 | @Override 277 | public TransitionHelper getTransitionHelper() { 278 | return transitionHelper; 279 | } 280 | 281 | @Override 282 | public void setTransitionHelper(TransitionHelper transitionHelper) { 283 | this.transitionHelper = transitionHelper; 284 | } 285 | 286 | @Override 287 | protected void onCreate(Bundle savedInstanceState) { 288 | TransitionHelper.init(this, savedInstanceState); 289 | TransitionHelper.of(this).addListener(this); 290 | super.onCreate(savedInstanceState); 291 | } 292 | 293 | @Override 294 | public void onSaveInstanceState(Bundle outState) { 295 | TransitionHelper.of(this).onSaveInstanceState(outState); 296 | super.onSaveInstanceState(outState); 297 | } 298 | 299 | @Override 300 | protected void onResume() { 301 | TransitionHelper.of(this).onResume(); 302 | super.onResume(); 303 | } 304 | 305 | @Override 306 | public void onBackPressed() { 307 | TransitionHelper.of(this).onBackPressed(); 308 | } 309 | 310 | 311 | @Override 312 | public void onBeforeViewShows(View contentView) { 313 | 314 | } 315 | 316 | @Override 317 | public void onBeforeEnter(View contentView) { 318 | 319 | } 320 | 321 | @Override 322 | public void onAfterEnter() { 323 | 324 | } 325 | 326 | @Override 327 | public boolean onBeforeBack() { 328 | return false; 329 | } 330 | 331 | @Override 332 | public void onBeforeReturn() { 333 | 334 | } 335 | } 336 | 337 | public static class BaseFragment extends Fragment implements TransitionHelper.Listener { 338 | 339 | @Override 340 | public void onCreate(Bundle savedInstanceState) { 341 | TransitionHelper.of(getActivity()).addListener(this); 342 | super.onCreate(savedInstanceState); 343 | 344 | } 345 | 346 | @Override 347 | public void onViewCreated(View view, Bundle savedInstanceState) { 348 | TransitionHelper.of(getActivity()).onViewCreated(); 349 | super.onViewCreated(view, savedInstanceState); 350 | } 351 | 352 | @Override 353 | public void onBeforeViewShows(View contentView) { 354 | 355 | } 356 | 357 | @Override 358 | public void onBeforeEnter(View contentView) { 359 | 360 | } 361 | 362 | @Override 363 | public void onAfterEnter() { 364 | 365 | } 366 | 367 | @Override 368 | public boolean onBeforeBack() { 369 | return false; 370 | } 371 | 372 | @Override 373 | public void onBeforeReturn() { 374 | 375 | } 376 | } 377 | 378 | 379 | } 380 | -------------------------------------------------------------------------------- /app/src/main/res/anim/scale_down.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/anim/scale_up.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_down.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_up.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circle_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circle_shadow.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 47 | 48 | 49 | 50 | 56 | 57 | 58 | 59 | 60 | 61 | 67 | 68 | 69 | 70 | 71 | 72 | 78 | 79 | 80 | 81 | 82 | 83 | 89 | 90 | 91 | 92 | 93 | 94 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_base.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 17 | 18 | 19 | 20 | 25 | 26 | 30 | 31 | 36 | 37 |