diff --git a/README.md b/README.md index 356370c..477432c 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,11 @@ It was designed to be used with the MVP pattern (try [Moxy](https://github.com/A + functionality is simple to extend + suitable for Unit Testing +## Version 2.+ ++ easy screen result subscription ++ predefined navigator ready for setup transition animation +**See the sample application** + ## How to add Add the dependency in your build.gradle: ```groovy @@ -140,30 +145,39 @@ This commands set will fulfill the needs of the most applications. But if you ne The library provides predefined navigators for _Fragments_ to use inside _Activity_. To use, just provide it with the container and _FragmentManager_ and override few simple methods. ```java -private Navigator navigator = new SupportFragmentNavigator( - getSupportFragmentManager(), R.id.main_container) { +private Navigator navigator = new SupportAppNavigator(this, R.id.container) { @Override - protected Fragment createFragment(String screenKey, Object data) { - return SampleFragment.getNewInstance((int) data); + protected Intent createActivityIntent(String screenKey, Object data) { + return null; } @Override - protected void showSystemMessage(String message) { - Toast.makeText(MainActivity.this, message, Toast.LENGTH_SHORT).show(); + protected Fragment createFragment(String screenKey, Object data) { + switch (screenKey) { + case Screens.PROFILE_SCREEN: + return new ProfileFragment(); + case Screens.SELECT_PHOTO_SCREEN: + return SelectPhotoFragment.getNewInstance((int) data); + } + return null; } @Override - protected void exit() { - finish(); + protected void setupFragmentTransactionAnimation( + Command command, + Fragment currentFragment, + Fragment nextFragment, + FragmentTransaction fragmentTransaction) { + //setup animation } }; ``` ## Sample To see how to add, initialize and use the library and predefined navigators check out the sample. -![](https://habrastorage.org/files/16d/2ee/6e3/16d2ee6e33a0428eb4f0dcab8ce6b294.gif) - -![](https://hsto.org/files/867/638/c33/867638c338704489b3107a6d7cb28c2d.gif) +![](https://habrastorage.org/web/a94/d73/653/a94d736534694d9daa994e0c260fca28.gif) +![](https://habrastorage.org/web/6dd/a19/15c/6dda1915cdcf4f14bed16fcffb3fd938.gif) +![](https://habrastorage.org/web/a63/881/7f8/a638817f8bba49daacc4fa427987fabb.gif) ## Participants + idea and code - Konstantin Tskhovrebov (@terrakok) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5cf815d..b9df631 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-all.zip diff --git a/library/build.gradle b/library/build.gradle index 340392d..81190ab 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -15,7 +15,7 @@ ext { bintrayName = 'cicerone' publishedGroupId = 'ru.terrakok.cicerone' artifact = 'cicerone' - libraryVersion = '1.2.1' + libraryVersion = '2.0.0' gitUrl = 'https://github.com/terrakok/Cicerone' allLicenses = ['MIT'] } diff --git a/library/src/main/java/ru/terrakok/cicerone/Router.java b/library/src/main/java/ru/terrakok/cicerone/Router.java index a4be260..572ffbc 100644 --- a/library/src/main/java/ru/terrakok/cicerone/Router.java +++ b/library/src/main/java/ru/terrakok/cicerone/Router.java @@ -1,10 +1,13 @@ package ru.terrakok.cicerone; +import java.util.HashMap; + import ru.terrakok.cicerone.commands.Back; import ru.terrakok.cicerone.commands.BackTo; import ru.terrakok.cicerone.commands.Forward; import ru.terrakok.cicerone.commands.Replace; import ru.terrakok.cicerone.commands.SystemMessage; +import ru.terrakok.cicerone.result.ResultListener; /** * Created by Konstantin Tckhovrebov (aka @terrakok) @@ -19,10 +22,49 @@ */ public class Router extends BaseRouter { + private HashMap resultListeners = new HashMap<>(); + public Router() { super(); } + /** + * Subscribe to the screen result.
+ * Note: only one listener can subscribe to a unique resultCode!
+ * You must call a removeResultListener() to avoid a memory leak. + * + * @param resultCode key for filter results + * @param listener result listener + */ + public void setResultListener(Integer resultCode, ResultListener listener) { + resultListeners.put(resultCode, listener); + } + + /** + * Unsubscribe from the screen result. + * + * @param resultCode key for filter results + */ + public void removeResultListener(Integer resultCode) { + resultListeners.remove(resultCode); + } + + /** + * Send result data to subscriber. + * + * @param resultCode result data key + * @param result result data + * @return TRUE if listener was notified and FALSE otherwise + */ + protected boolean sendResult(Integer resultCode, Object result) { + ResultListener resultListener = resultListeners.get(resultCode); + if (resultListener != null) { + resultListener.onResult(result); + return true; + } + return false; + } + /** * Open new screen and add it to the screens chain. * @@ -138,6 +180,17 @@ public void exit() { executeCommand(new Back()); } + /** + * Return to the previous screen in the chain and send result data. + * + * @param resultCode result data key + * @param result result data + */ + public void exitWithResult(Integer resultCode, Object result) { + exit(); + sendResult(resultCode, result); + } + /** * Return to the previous screen in the chain and show system message. * diff --git a/library/src/main/java/ru/terrakok/cicerone/android/FragmentNavigator.java b/library/src/main/java/ru/terrakok/cicerone/android/FragmentNavigator.java index a35db91..b713139 100644 --- a/library/src/main/java/ru/terrakok/cicerone/android/FragmentNavigator.java +++ b/library/src/main/java/ru/terrakok/cicerone/android/FragmentNavigator.java @@ -2,6 +2,7 @@ import android.app.Fragment; import android.app.FragmentManager; +import android.app.FragmentTransaction; import ru.terrakok.cicerone.Navigator; import ru.terrakok.cicerone.commands.Back; @@ -42,6 +43,21 @@ public FragmentNavigator(FragmentManager fragmentManager, int containerId) { this.containerId = containerId; } + /** + * Override this method for setup custom fragment transaction animation. + * + * @param command current navigation command. Maybe only {@link Forward} and {@link Replace} + * @param currentFragment current fragment in container + * (for {@link Replace} command it will be previous screen, NOT replaced screen) + * @param nextFragment next screen fragment + * @param fragmentTransaction fragment transaction + */ + protected void setupFragmentTransactionAnimation(Command command, + Fragment currentFragment, + Fragment nextFragment, + FragmentTransaction fragmentTransaction) { + } + @Override public void applyCommand(Command command) { if (command instanceof Forward) { @@ -51,8 +67,16 @@ public void applyCommand(Command command) { unknownScreen(command); return; } - fragmentManager - .beginTransaction() + + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + setupFragmentTransactionAnimation( + command, + fragmentManager.findFragmentById(containerId), + fragment, + fragmentTransaction + ); + + fragmentTransaction .replace(containerId, fragment) .addToBackStack(forward.getScreenKey()) .commit(); @@ -71,14 +95,29 @@ public void applyCommand(Command command) { } if (fragmentManager.getBackStackEntryCount() > 0) { fragmentManager.popBackStackImmediate(); - fragmentManager - .beginTransaction() + + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + setupFragmentTransactionAnimation( + command, + fragmentManager.findFragmentById(containerId), + fragment, + fragmentTransaction + ); + + fragmentTransaction .replace(containerId, fragment) .addToBackStack(replace.getScreenKey()) .commit(); } else { - fragmentManager - .beginTransaction() + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + setupFragmentTransactionAnimation( + command, + fragmentManager.findFragmentById(containerId), + fragment, + fragmentTransaction + ); + + fragmentTransaction .replace(containerId, fragment) .commit(); } @@ -106,10 +145,7 @@ public void applyCommand(Command command) { } private void backToRoot() { - for (int i = 0; i < fragmentManager.getBackStackEntryCount(); i++) { - fragmentManager.popBackStack(); - } - fragmentManager.executePendingTransactions(); + fragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); } /** diff --git a/library/src/main/java/ru/terrakok/cicerone/android/SupportFragmentNavigator.java b/library/src/main/java/ru/terrakok/cicerone/android/SupportFragmentNavigator.java index e838611..7a89776 100644 --- a/library/src/main/java/ru/terrakok/cicerone/android/SupportFragmentNavigator.java +++ b/library/src/main/java/ru/terrakok/cicerone/android/SupportFragmentNavigator.java @@ -2,6 +2,7 @@ import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; import ru.terrakok.cicerone.Navigator; import ru.terrakok.cicerone.commands.Back; @@ -42,6 +43,21 @@ public SupportFragmentNavigator(FragmentManager fragmentManager, int containerId this.containerId = containerId; } + /** + * Override this method for setup custom fragment transaction animation. + * + * @param command current navigation command. Maybe only {@link Forward} and {@link Replace} + * @param currentFragment current fragment in container + * (for {@link Replace} command it will be previous screen, NOT replaced screen) + * @param nextFragment next screen fragment + * @param fragmentTransaction fragment transaction + */ + protected void setupFragmentTransactionAnimation(Command command, + Fragment currentFragment, + Fragment nextFragment, + FragmentTransaction fragmentTransaction) { + } + @Override public void applyCommand(Command command) { if (command instanceof Forward) { @@ -51,8 +67,16 @@ public void applyCommand(Command command) { unknownScreen(command); return; } - fragmentManager - .beginTransaction() + + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + setupFragmentTransactionAnimation( + command, + fragmentManager.findFragmentById(containerId), + fragment, + fragmentTransaction + ); + + fragmentTransaction .replace(containerId, fragment) .addToBackStack(forward.getScreenKey()) .commit(); @@ -71,14 +95,29 @@ public void applyCommand(Command command) { } if (fragmentManager.getBackStackEntryCount() > 0) { fragmentManager.popBackStackImmediate(); - fragmentManager - .beginTransaction() + + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + setupFragmentTransactionAnimation( + command, + fragmentManager.findFragmentById(containerId), + fragment, + fragmentTransaction + ); + + fragmentTransaction .replace(containerId, fragment) .addToBackStack(replace.getScreenKey()) .commit(); } else { - fragmentManager - .beginTransaction() + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + setupFragmentTransactionAnimation( + command, + fragmentManager.findFragmentById(containerId), + fragment, + fragmentTransaction + ); + + fragmentTransaction .replace(containerId, fragment) .commit(); } @@ -106,10 +145,7 @@ public void applyCommand(Command command) { } private void backToRoot() { - for (int i = 0; i < fragmentManager.getBackStackEntryCount(); i++) { - fragmentManager.popBackStack(); - } - fragmentManager.executePendingTransactions(); + fragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); } /** diff --git a/library/src/main/java/ru/terrakok/cicerone/result/ResultListener.java b/library/src/main/java/ru/terrakok/cicerone/result/ResultListener.java new file mode 100644 index 0000000..1cecd05 --- /dev/null +++ b/library/src/main/java/ru/terrakok/cicerone/result/ResultListener.java @@ -0,0 +1,15 @@ +package ru.terrakok.cicerone.result; + +/** + * @author Konstantin Tskhovrebov (aka terrakok) on 04.07.17. + */ + +public interface ResultListener { + + /** + * Received result from screen. + * + * @param resultData + */ + void onResult(Object resultData); +} diff --git a/library/stub-android/src/main/java/android/app/FragmentManager.java b/library/stub-android/src/main/java/android/app/FragmentManager.java index 4356801..f3cb746 100644 --- a/library/stub-android/src/main/java/android/app/FragmentManager.java +++ b/library/stub-android/src/main/java/android/app/FragmentManager.java @@ -4,6 +4,7 @@ * Created by laomo on 2016-11-21. */ public class FragmentManager { + public static final int POP_BACK_STACK_INCLUSIVE = 1<<0; public FragmentTransaction beginTransaction() { throw new RuntimeException("Stub!"); @@ -33,6 +34,10 @@ public BackStackEntry getBackStackEntryAt(int index) { throw new RuntimeException("Stub!"); } + public Fragment findFragmentById(int id) { + throw new RuntimeException("Stub!"); + } + public interface BackStackEntry { int getId(); diff --git a/library/stub-android/src/main/java/android/support/v4/app/FragmentManager.java b/library/stub-android/src/main/java/android/support/v4/app/FragmentManager.java index b131312..cfac984 100644 --- a/library/stub-android/src/main/java/android/support/v4/app/FragmentManager.java +++ b/library/stub-android/src/main/java/android/support/v4/app/FragmentManager.java @@ -6,6 +6,7 @@ */ public class FragmentManager { + public static final int POP_BACK_STACK_INCLUSIVE = 1<<0; public FragmentTransaction beginTransaction() { throw new RuntimeException("Stub!"); @@ -35,6 +36,9 @@ public BackStackEntry getBackStackEntryAt(int index) { throw new RuntimeException("Stub!"); } + public Fragment findFragmentById(int id) { + throw new RuntimeException("Stub!"); + } public interface BackStackEntry { int getId(); diff --git a/sample/build.gradle b/sample/build.gradle index 2e34594..940f106 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -9,7 +9,7 @@ android { buildToolsVersion '25.0.2' defaultConfig { - minSdkVersion 16 + minSdkVersion 19 targetSdkVersion 25 versionCode 1 versionName "1.0.0" diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index a20c05e..16c0016 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -20,5 +20,6 @@ + diff --git a/sample/src/main/java/ru/terrakok/cicerone/sample/Screens.java b/sample/src/main/java/ru/terrakok/cicerone/sample/Screens.java index 749b578..c74ea00 100644 --- a/sample/src/main/java/ru/terrakok/cicerone/sample/Screens.java +++ b/sample/src/main/java/ru/terrakok/cicerone/sample/Screens.java @@ -18,4 +18,7 @@ public class Screens { public static final String FORWARD_SCREEN = "forward screen"; public static final String GITHUB_SCREEN = "github screen"; + + public static final String PROFILE_SCREEN = "profile screen"; + public static final String SELECT_PHOTO_SCREEN = "select photo screen"; } diff --git a/sample/src/main/java/ru/terrakok/cicerone/sample/dagger/AppComponent.java b/sample/src/main/java/ru/terrakok/cicerone/sample/dagger/AppComponent.java index c3fbbb6..0646275 100644 --- a/sample/src/main/java/ru/terrakok/cicerone/sample/dagger/AppComponent.java +++ b/sample/src/main/java/ru/terrakok/cicerone/sample/dagger/AppComponent.java @@ -5,6 +5,9 @@ import dagger.Component; import ru.terrakok.cicerone.sample.dagger.module.LocalNavigationModule; import ru.terrakok.cicerone.sample.dagger.module.NavigationModule; +import ru.terrakok.cicerone.sample.ui.animations.ProfileActivity; +import ru.terrakok.cicerone.sample.ui.animations.photos.SelectPhotoFragment; +import ru.terrakok.cicerone.sample.ui.animations.profile.ProfileFragment; import ru.terrakok.cicerone.sample.ui.bottom.BottomNavigationActivity; import ru.terrakok.cicerone.sample.ui.bottom.TabContainerFragment; import ru.terrakok.cicerone.sample.ui.main.MainActivity; @@ -31,4 +34,10 @@ public interface AppComponent { void inject(BottomNavigationActivity activity); void inject(TabContainerFragment fragment); + + void inject(ProfileFragment fragment); + + void inject(SelectPhotoFragment fragment); + + void inject(ProfileActivity activity); } diff --git a/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/animation/photos/SelectPhotoPresenter.java b/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/animation/photos/SelectPhotoPresenter.java new file mode 100644 index 0000000..1724143 --- /dev/null +++ b/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/animation/photos/SelectPhotoPresenter.java @@ -0,0 +1,42 @@ +package ru.terrakok.cicerone.sample.mvp.animation.photos; + +import com.arellomobile.mvp.InjectViewState; +import com.arellomobile.mvp.MvpPresenter; + +import ru.terrakok.cicerone.Router; +import ru.terrakok.cicerone.sample.R; + +/** + * Created by Konstantin Tskhovrebov (aka @terrakok) on 14.07.17. + */ + +@InjectViewState +public class SelectPhotoPresenter extends MvpPresenter { + private Router router; + private final int RESULT_CODE; + + public SelectPhotoPresenter(Router router, int resultCode) { + this.router = router; + RESULT_CODE = resultCode; + } + + @Override + protected void onFirstViewAttach() { + super.onFirstViewAttach(); + + getViewState().showPhotos(new int[] { + R.drawable.ava_1, + R.drawable.ava_2, + R.drawable.ava_3, + R.drawable.ava_4 + }); + } + + public void onPhotoClick(int photoRes) { + router.exitWithResult(RESULT_CODE, photoRes); + } + + public void onBackPressed() { + router.exit(); + } +} diff --git a/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/animation/photos/SelectPhotoView.java b/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/animation/photos/SelectPhotoView.java new file mode 100644 index 0000000..78f6c78 --- /dev/null +++ b/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/animation/photos/SelectPhotoView.java @@ -0,0 +1,14 @@ +package ru.terrakok.cicerone.sample.mvp.animation.photos; + +import com.arellomobile.mvp.MvpView; +import com.arellomobile.mvp.viewstate.strategy.AddToEndSingleStrategy; +import com.arellomobile.mvp.viewstate.strategy.StateStrategyType; + +/** + * Created by Konstantin Tskhovrebov (aka @terrakok) on 14.07.17. + */ + +@StateStrategyType(AddToEndSingleStrategy.class) +public interface SelectPhotoView extends MvpView { + void showPhotos(int[] resurceIds); +} diff --git a/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/animation/profile/ProfilePresenter.java b/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/animation/profile/ProfilePresenter.java new file mode 100644 index 0000000..c63d808 --- /dev/null +++ b/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/animation/profile/ProfilePresenter.java @@ -0,0 +1,57 @@ +package ru.terrakok.cicerone.sample.mvp.animation.profile; + +import com.arellomobile.mvp.InjectViewState; +import com.arellomobile.mvp.MvpPresenter; + +import ru.terrakok.cicerone.Router; +import ru.terrakok.cicerone.result.ResultListener; +import ru.terrakok.cicerone.sample.Screens; + +/** + * Created by Konstantin Tskhovrebov (aka @terrakok) on 14.07.17. + */ + +@InjectViewState +public class ProfilePresenter extends MvpPresenter { + private static final int PHOTO_RESULT_CODE = 42; + + private Router router; + private int currentPhoto; + + public ProfilePresenter(int defaultPhoto, Router router) { + currentPhoto = defaultPhoto; + this.router = router; + + router.setResultListener(PHOTO_RESULT_CODE, new ResultListener() { + @Override + public void onResult(Object resultData) { + currentPhoto = (int) resultData; + updatePhoto(); + } + }); + } + + @Override + protected void onFirstViewAttach() { + super.onFirstViewAttach(); + updatePhoto(); + } + + @Override + public void onDestroy() { + router.removeResultListener(PHOTO_RESULT_CODE); + super.onDestroy(); + } + + private void updatePhoto() { + getViewState().showPhoto(currentPhoto); + } + + public void onPhotoClicked() { + router.navigateTo(Screens.SELECT_PHOTO_SCREEN, PHOTO_RESULT_CODE); + } + + public void onBackPressed() { + router.exit(); + } +} diff --git a/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/animation/profile/ProfileView.java b/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/animation/profile/ProfileView.java new file mode 100644 index 0000000..ad62ba8 --- /dev/null +++ b/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/animation/profile/ProfileView.java @@ -0,0 +1,14 @@ +package ru.terrakok.cicerone.sample.mvp.animation.profile; + +import com.arellomobile.mvp.MvpView; +import com.arellomobile.mvp.viewstate.strategy.AddToEndSingleStrategy; +import com.arellomobile.mvp.viewstate.strategy.StateStrategyType; + +/** + * Created by Konstantin Tskhovrebov (aka @terrakok) on 14.07.17. + */ + +@StateStrategyType(AddToEndSingleStrategy.class) +public interface ProfileView extends MvpView { + void showPhoto(int resId); +} diff --git a/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/main/SamplePresenter.java b/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/main/SamplePresenter.java index 7650695..63c2337 100644 --- a/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/main/SamplePresenter.java +++ b/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/main/SamplePresenter.java @@ -60,6 +60,7 @@ public void onForwardWithDelayCommandClick() { future = executorService.schedule(new Runnable() { @Override public void run() { + //WARNING! Navigation must be only in UI thread. this method works only for sample :) router.navigateTo(Screens.SAMPLE_SCREEN + (screenNumber + 1), screenNumber + 1); } }, 5, TimeUnit.SECONDS); diff --git a/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/start/StartActivityPresenter.java b/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/start/StartActivityPresenter.java index 6e3b68b..dec576d 100644 --- a/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/start/StartActivityPresenter.java +++ b/sample/src/main/java/ru/terrakok/cicerone/sample/mvp/start/StartActivityPresenter.java @@ -23,6 +23,10 @@ public void onMultiPressed() { router.navigateTo(Screens.BOTTOM_NAVIGATION_ACTIVITY_SCREEN); } + public void onResultWithAnimationPressed() { + router.navigateTo(Screens.PROFILE_SCREEN); + } + public void onBackPressed() { router.exit(); } diff --git a/sample/src/main/java/ru/terrakok/cicerone/sample/ui/animations/ProfileActivity.java b/sample/src/main/java/ru/terrakok/cicerone/sample/ui/animations/ProfileActivity.java new file mode 100644 index 0000000..c443959 --- /dev/null +++ b/sample/src/main/java/ru/terrakok/cicerone/sample/ui/animations/ProfileActivity.java @@ -0,0 +1,117 @@ +package ru.terrakok.cicerone.sample.ui.animations; + +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.support.v7.app.AppCompatActivity; +import android.transition.ChangeBounds; +import android.view.View; + +import javax.inject.Inject; + +import ru.terrakok.cicerone.Navigator; +import ru.terrakok.cicerone.NavigatorHolder; +import ru.terrakok.cicerone.android.SupportAppNavigator; +import ru.terrakok.cicerone.commands.Command; +import ru.terrakok.cicerone.commands.Forward; +import ru.terrakok.cicerone.commands.Replace; +import ru.terrakok.cicerone.sample.R; +import ru.terrakok.cicerone.sample.SampleApplication; +import ru.terrakok.cicerone.sample.Screens; +import ru.terrakok.cicerone.sample.ui.animations.photos.SelectPhotoFragment; +import ru.terrakok.cicerone.sample.ui.animations.profile.ProfileFragment; +import ru.terrakok.cicerone.sample.ui.common.BackButtonListener; + +/** + * Created by Konstantin Tskhovrebov (aka @terrakok) on 14.07.17. + */ + +public class ProfileActivity extends AppCompatActivity { + public static final String PHOTO_TRANSITION = "photo_trasition"; + + @Inject + NavigatorHolder navigatorHolder; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + SampleApplication.INSTANCE.getAppComponent().inject(this); + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_container); + + if (savedInstanceState == null) { + navigator.applyCommand(new Replace(Screens.PROFILE_SCREEN, null)); + } + } + + @Override + protected void onResumeFragments() { + super.onResumeFragments(); + navigatorHolder.setNavigator(navigator); + } + + @Override + protected void onPause() { + navigatorHolder.removeNavigator(); + super.onPause(); + } + + private Navigator navigator = new SupportAppNavigator(this, R.id.container) { + @Override + protected Intent createActivityIntent(String screenKey, Object data) { + return null; + } + + @Override + protected Fragment createFragment(String screenKey, Object data) { + switch (screenKey) { + case Screens.PROFILE_SCREEN: + return new ProfileFragment(); + case Screens.SELECT_PHOTO_SCREEN: + return SelectPhotoFragment.getNewInstance((int) data); + } + return null; + } + + @Override + protected void setupFragmentTransactionAnimation(Command command, Fragment currentFragment, Fragment nextFragment, FragmentTransaction fragmentTransaction) { + if (command instanceof Forward + && currentFragment instanceof ProfileFragment + && nextFragment instanceof SelectPhotoFragment) { + setupSharedElementForProfileToSelectPhoto( + (ProfileFragment) currentFragment, + (SelectPhotoFragment) nextFragment, + fragmentTransaction + ); + } + } + }; + + private void setupSharedElementForProfileToSelectPhoto(ProfileFragment profileFragment, + SelectPhotoFragment selectPhotoFragment, + FragmentTransaction fragmentTransaction) { + ChangeBounds changeBounds = new ChangeBounds(); + selectPhotoFragment.setSharedElementEnterTransition(changeBounds); + selectPhotoFragment.setSharedElementReturnTransition(changeBounds); + profileFragment.setSharedElementEnterTransition(changeBounds); + profileFragment.setSharedElementReturnTransition(changeBounds); + + View view = profileFragment.getAvatarViewForAnimation(); + fragmentTransaction.addSharedElement(view, PHOTO_TRANSITION); + selectPhotoFragment.setAnimationDestinationId((Integer) view.getTag()); + } + + @Override + public void onBackPressed() { + Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.container); + if (fragment != null + && fragment instanceof BackButtonListener + && ((BackButtonListener) fragment).onBackPressed()) { + return; + } else { + super.onBackPressed(); + } + } +} diff --git a/sample/src/main/java/ru/terrakok/cicerone/sample/ui/animations/photos/SelectPhotoFragment.java b/sample/src/main/java/ru/terrakok/cicerone/sample/ui/animations/photos/SelectPhotoFragment.java new file mode 100644 index 0000000..b187b73 --- /dev/null +++ b/sample/src/main/java/ru/terrakok/cicerone/sample/ui/animations/photos/SelectPhotoFragment.java @@ -0,0 +1,135 @@ +package ru.terrakok.cicerone.sample.ui.animations.photos; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.arellomobile.mvp.MvpAppCompatFragment; +import com.arellomobile.mvp.presenter.InjectPresenter; +import com.arellomobile.mvp.presenter.ProvidePresenter; + +import javax.inject.Inject; + +import ru.terrakok.cicerone.Router; +import ru.terrakok.cicerone.sample.R; +import ru.terrakok.cicerone.sample.SampleApplication; +import ru.terrakok.cicerone.sample.mvp.animation.photos.SelectPhotoPresenter; +import ru.terrakok.cicerone.sample.mvp.animation.photos.SelectPhotoView; +import ru.terrakok.cicerone.sample.ui.animations.ProfileActivity; +import ru.terrakok.cicerone.sample.ui.common.BackButtonListener; + +/** + * Created by Konstantin Tskhovrebov (aka @terrakok) on 14.07.17. + */ + +public class SelectPhotoFragment extends MvpAppCompatFragment implements SelectPhotoView, BackButtonListener { + private static final String ARG_RESULT_CODE = "arg_result_code"; + private static final String ARG_ANIM_DESTINATION = "arg_anim_dest"; + + private ImageView photo1; + private ImageView photo2; + private ImageView photo3; + private ImageView photo4; + + @Inject + Router router; + + @InjectPresenter + SelectPhotoPresenter presenter; + + public static SelectPhotoFragment getNewInstance(int resultCode) { + SelectPhotoFragment fragment = new SelectPhotoFragment(); + Bundle args = new Bundle(); + args.putInt(ARG_RESULT_CODE, resultCode); + fragment.setArguments(args); + return fragment; + } + + public void setAnimationDestinationId(int resId) { + Bundle arguments = getArguments(); + arguments.putInt(ARG_ANIM_DESTINATION, resId); + setArguments(arguments); + } + + private int getAnimationDestionationId() { + return getArguments().getInt(ARG_ANIM_DESTINATION); + } + + @ProvidePresenter + SelectPhotoPresenter providePresenter() { + return new SelectPhotoPresenter(router, getArguments().getInt(ARG_RESULT_CODE)); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + SampleApplication.INSTANCE.getAppComponent().inject(this); + super.onCreate(savedInstanceState); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_select_photo, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + photo1 = (ImageView) view.findViewById(R.id.select_image_1); + photo2 = (ImageView) view.findViewById(R.id.select_image_2); + photo3 = (ImageView) view.findViewById(R.id.select_image_3); + photo4 = (ImageView) view.findViewById(R.id.select_image_4); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + photo1.setOnClickListener(clickListener); + photo2.setOnClickListener(clickListener); + photo3.setOnClickListener(clickListener); + photo4.setOnClickListener(clickListener); + } + + private View.OnClickListener clickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + photo1.setTransitionName(null); + photo2.setTransitionName(null); + photo3.setTransitionName(null); + photo4.setTransitionName(null); + v.setTransitionName(ProfileActivity.PHOTO_TRANSITION); + presenter.onPhotoClick((Integer) v.getTag()); + } + }; + + @Override + public void showPhotos(int[] resurceIds) { + if (resurceIds.length >= 4) { + photo1.setImageResource(resurceIds[0]); + photo2.setImageResource(resurceIds[1]); + photo3.setImageResource(resurceIds[2]); + photo4.setImageResource(resurceIds[3]); + + photo1.setTag(resurceIds[0]); + photo2.setTag(resurceIds[1]); + photo3.setTag(resurceIds[2]); + photo4.setTag(resurceIds[3]); + + //for shared element animation + int animRes = getAnimationDestionationId(); + photo1.setTransitionName(animRes == resurceIds[0] ? ProfileActivity.PHOTO_TRANSITION : null); + photo2.setTransitionName(animRes == resurceIds[1] ? ProfileActivity.PHOTO_TRANSITION : null); + photo3.setTransitionName(animRes == resurceIds[2] ? ProfileActivity.PHOTO_TRANSITION : null); + photo4.setTransitionName(animRes == resurceIds[3] ? ProfileActivity.PHOTO_TRANSITION : null); + } + } + + @Override + public boolean onBackPressed() { + presenter.onBackPressed(); + return true; + } +} diff --git a/sample/src/main/java/ru/terrakok/cicerone/sample/ui/animations/profile/ProfileFragment.java b/sample/src/main/java/ru/terrakok/cicerone/sample/ui/animations/profile/ProfileFragment.java new file mode 100644 index 0000000..59e0806 --- /dev/null +++ b/sample/src/main/java/ru/terrakok/cicerone/sample/ui/animations/profile/ProfileFragment.java @@ -0,0 +1,89 @@ +package ru.terrakok.cicerone.sample.ui.animations.profile; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.arellomobile.mvp.MvpAppCompatFragment; +import com.arellomobile.mvp.presenter.InjectPresenter; +import com.arellomobile.mvp.presenter.ProvidePresenter; + +import javax.inject.Inject; + +import ru.terrakok.cicerone.Router; +import ru.terrakok.cicerone.sample.R; +import ru.terrakok.cicerone.sample.SampleApplication; +import ru.terrakok.cicerone.sample.mvp.animation.profile.ProfilePresenter; +import ru.terrakok.cicerone.sample.mvp.animation.profile.ProfileView; +import ru.terrakok.cicerone.sample.ui.animations.ProfileActivity; +import ru.terrakok.cicerone.sample.ui.common.BackButtonListener; + +/** + * Created by Konstantin Tskhovrebov (aka @terrakok) on 14.07.17. + */ + +public class ProfileFragment extends MvpAppCompatFragment implements ProfileView, BackButtonListener { + private ImageView avatar; + + @Inject + Router router; + + @InjectPresenter + ProfilePresenter presenter; + + @ProvidePresenter + ProfilePresenter providePresenter() { + return new ProfilePresenter(R.drawable.ava_1, router); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + SampleApplication.INSTANCE.getAppComponent().inject(this); + super.onCreate(savedInstanceState); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_profile, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + avatar = (ImageView) view.findViewById(R.id.avatar_imageView); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + avatar.setTransitionName(ProfileActivity.PHOTO_TRANSITION); + avatar.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + presenter.onPhotoClicked(); + } + }); + } + + @Override + public void showPhoto(int resId) { + avatar.setImageResource(resId); + + //for shared element animation + avatar.setTag(resId); + } + + public View getAvatarViewForAnimation() { + return avatar; + } + + @Override + public boolean onBackPressed() { + presenter.onBackPressed(); + return true; + } +} diff --git a/sample/src/main/java/ru/terrakok/cicerone/sample/ui/start/StartActivity.java b/sample/src/main/java/ru/terrakok/cicerone/sample/ui/start/StartActivity.java index 4ee3f6f..b7b296a 100644 --- a/sample/src/main/java/ru/terrakok/cicerone/sample/ui/start/StartActivity.java +++ b/sample/src/main/java/ru/terrakok/cicerone/sample/ui/start/StartActivity.java @@ -25,6 +25,7 @@ import ru.terrakok.cicerone.sample.Screens; import ru.terrakok.cicerone.sample.mvp.start.StartActivityPresenter; import ru.terrakok.cicerone.sample.mvp.start.StartActivityView; +import ru.terrakok.cicerone.sample.ui.animations.ProfileActivity; import ru.terrakok.cicerone.sample.ui.bottom.BottomNavigationActivity; import ru.terrakok.cicerone.sample.ui.main.MainActivity; @@ -68,6 +69,12 @@ public void onClick(View view) { presenter.onMultiPressed(); } }); + findViewById(R.id.result_and_anim_button).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + presenter.onResultWithAnimationPressed(); + } + }); } @Override @@ -87,6 +94,7 @@ public void onBackPressed() { presenter.onBackPressed(); } + //Sample fully custom navigator: private Navigator navigator = new Navigator() { @Override public void applyCommand(Command command) { @@ -114,6 +122,9 @@ private void forward(Forward command) { case Screens.BOTTOM_NAVIGATION_ACTIVITY_SCREEN: startActivity(new Intent(StartActivity.this, BottomNavigationActivity.class)); break; + case Screens.PROFILE_SCREEN: + startActivity(new Intent(StartActivity.this, ProfileActivity.class)); + break; default: Log.e("Cicerone", "Unknown screen: " + command.getScreenKey()); break; @@ -125,6 +136,7 @@ private void replace(Replace command) { case Screens.START_ACTIVITY_SCREEN: case Screens.MAIN_ACTIVITY_SCREEN: case Screens.BOTTOM_NAVIGATION_ACTIVITY_SCREEN: + case Screens.PROFILE_SCREEN: forward(new Forward(command.getScreenKey(), command.getTransitionData())); finish(); break; diff --git a/sample/src/main/res/drawable-nodpi/ava_1.png b/sample/src/main/res/drawable-nodpi/ava_1.png new file mode 100644 index 0000000..b3d157c Binary files /dev/null and b/sample/src/main/res/drawable-nodpi/ava_1.png differ diff --git a/sample/src/main/res/drawable-nodpi/ava_2.png b/sample/src/main/res/drawable-nodpi/ava_2.png new file mode 100644 index 0000000..c8a66b8 Binary files /dev/null and b/sample/src/main/res/drawable-nodpi/ava_2.png differ diff --git a/sample/src/main/res/drawable-nodpi/ava_3.png b/sample/src/main/res/drawable-nodpi/ava_3.png new file mode 100644 index 0000000..941c625 Binary files /dev/null and b/sample/src/main/res/drawable-nodpi/ava_3.png differ diff --git a/sample/src/main/res/drawable-nodpi/ava_4.png b/sample/src/main/res/drawable-nodpi/ava_4.png new file mode 100644 index 0000000..813b28d Binary files /dev/null and b/sample/src/main/res/drawable-nodpi/ava_4.png differ diff --git a/sample/src/main/res/layout/activity_container.xml b/sample/src/main/res/layout/activity_container.xml new file mode 100644 index 0000000..cdd5c46 --- /dev/null +++ b/sample/src/main/res/layout/activity_container.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_start.xml b/sample/src/main/res/layout/activity_start.xml index 12dec01..dadda39 100644 --- a/sample/src/main/res/layout/activity_start.xml +++ b/sample/src/main/res/layout/activity_start.xml @@ -36,4 +36,11 @@ android:layout_height="wrap_content" android:layout_margin="16dp" android:text="@string/multi_nav"/> + +