diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java index 9812962f1..bd86d8747 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.java @@ -31,6 +31,7 @@ import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; import com.hippo.ehviewer.R; import com.hippo.ehviewer.data.ListUrlBuilder; @@ -38,6 +39,7 @@ import com.hippo.ehviewer.ui.ContentActivity; import com.hippo.ehviewer.widget.ContentLayout; import com.hippo.ehviewer.widget.SearchLayout; import com.hippo.scene.Scene; +import com.hippo.scene.TransitionCurtain; import com.hippo.widget.Appbar; public class GalleryListScene extends Scene implements SearchLayout.SearhLayoutHelper, @@ -81,6 +83,16 @@ public class GalleryListScene extends Scene implements SearchLayout.SearhLayoutH // TODO mListView = inflater.inflate(R.layout.fragment_list, null); + Button bb = (Button) mListView.findViewById(R.id.test_button); + bb.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + startScene(TestScene.class, null, new TransitionCurtain(new TransitionCurtain.ViewPair[] { + new TransitionCurtain.ViewPair(R.id.haha, R.id.haha) + })); + } + }); + // Main View View sceneView = inflater.inflate(R.layout.scene_gallery_list, null); mAppbar = (Appbar) sceneView.findViewById(R.id.appbar); diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/TestScene.java b/app/src/main/java/com/hippo/ehviewer/ui/scene/TestScene.java new file mode 100644 index 000000000..8f58c26f9 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/TestScene.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.ehviewer.ui.scene; + +import android.content.res.Resources; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; + +import com.hippo.ehviewer.R; +import com.hippo.ehviewer.ui.ContentActivity; +import com.hippo.scene.Scene; + +public class TestScene extends Scene { + + private ContentActivity mActivity; + private Resources mResources; + + @Override + public void onCreate(Bundle savedInstanceState) { + mActivity = (ContentActivity) getStageActivity(); + mResources = mActivity.getResources(); + } + + @SuppressWarnings("InflateParams") + public View onCreateSceneView(Bundle savedInstanceState) { + LayoutInflater inflater = mActivity.getLayoutInflater(); + + return inflater.inflate(R.layout.scene_test, null); + } +} diff --git a/app/src/main/java/com/hippo/scene/Announcer.java b/app/src/main/java/com/hippo/scene/Announcer.java index 5836e9c24..758088771 100644 --- a/app/src/main/java/com/hippo/scene/Announcer.java +++ b/app/src/main/java/com/hippo/scene/Announcer.java @@ -17,12 +17,13 @@ package com.hippo.scene; import android.os.Bundle; import android.os.Parcelable; +import android.support.annotation.Nullable; import java.io.Serializable; public class Announcer { - private Bundle mExtras; + private @Nullable Bundle mExtras; public boolean getBooleanExtra(String name, boolean defaultValue) { return mExtras == null ? defaultValue : diff --git a/app/src/main/java/com/hippo/scene/Curtain.java b/app/src/main/java/com/hippo/scene/Curtain.java index d11999eb9..6c8ad54a2 100644 --- a/app/src/main/java/com/hippo/scene/Curtain.java +++ b/app/src/main/java/com/hippo/scene/Curtain.java @@ -15,5 +15,20 @@ package com.hippo.scene; -public class Curtain { +import android.support.annotation.NonNull; + +public abstract class Curtain { + + public abstract void open(@NonNull Scene enter, @NonNull Scene exit); + + public abstract void close(@NonNull Scene enter, @NonNull Scene exit); + + /** + * Call it after {@link #close} + * + * @param scene the scene + */ + protected void dispatchDetachFromeStage(Scene scene) { + scene.detachFromeStage(); + } } diff --git a/app/src/main/java/com/hippo/scene/Scene.java b/app/src/main/java/com/hippo/scene/Scene.java index f537f5a61..86cfa3aab 100644 --- a/app/src/main/java/com/hippo/scene/Scene.java +++ b/app/src/main/java/com/hippo/scene/Scene.java @@ -44,18 +44,27 @@ public abstract class Scene { } // If there is no StageActivity for SceneManager, yout will get AssertError - public StageActivity getStageActivity() { + public @NonNull StageActivity getStageActivity() { StageActivity stageActivity = sSceneManager.getStageActivity(); AssertUtils.assertNotNull("StageActivity is null", stageActivity); return stageActivity; } - View getSceneView() { + @Nullable View getSceneView() { return mSceneView; } + public void startScene(@NonNull Class sceneClass) { + sSceneManager.startScene(sceneClass, null, null); + } + public void startScene(@NonNull Class sceneClass, @Nullable Announcer announcer) { - sSceneManager.startScene(sceneClass, announcer); + sSceneManager.startScene(sceneClass, announcer, null); + } + + public void startScene(@NonNull Class sceneClass, @Nullable Announcer announcer, + @Nullable Curtain curtain) { + sSceneManager.startScene(sceneClass, announcer, curtain); } public final void finish() { @@ -78,11 +87,13 @@ public abstract class Scene { onPause(); } - void destroy() { + void destroy(boolean removeScene) { onDestroy(); - // Make sure scene view is detached from stage - detachFromeStage(); + if (removeScene) { + // Make sure scene view is detached from stage + detachFromeStage(); + } } /** diff --git a/app/src/main/java/com/hippo/scene/SceneManager.java b/app/src/main/java/com/hippo/scene/SceneManager.java index 4039283ff..e1147e01e 100644 --- a/app/src/main/java/com/hippo/scene/SceneManager.java +++ b/app/src/main/java/com/hippo/scene/SceneManager.java @@ -18,6 +18,7 @@ package com.hippo.scene; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.util.Pair; import com.hippo.util.Log; @@ -27,7 +28,7 @@ class SceneManager { private static final String TAG = SceneManager.class.getSimpleName(); - private Stack mSceneStack = new Stack<>(); + private Stack> mSceneStack = new Stack<>(); private StageActivity mStageActivity; // Should only be called by SceneApplication @@ -42,7 +43,8 @@ class SceneManager { return mStageActivity; } - void startScene(@NonNull Class sceneClass, @Nullable Announcer announcer) { + void startScene(@NonNull Class sceneClass, @Nullable Announcer announcer, + @Nullable Curtain curtain) { Scene scene; try { scene = (Scene) sceneClass.newInstance(); @@ -55,25 +57,55 @@ class SceneManager { throw new IllegalStateException(sceneClass.getName() + " can not cast to scene"); } - Scene topScene = getTopState(); - if (topScene != null) { - topScene.pause(); + Scene previousState = getTopState(); + if (previousState != null) { + previousState.pause(); } - mSceneStack.push(scene); + mSceneStack.push(new Pair<>(scene, curtain)); scene.create(null); scene.resume(); + + // Do animation via curtain + if (curtain != null && previousState != null) { + curtain.open(scene, previousState); + } } void finishScene(@NonNull Scene scene) { - if (mSceneStack.remove(scene)) { - scene.destroy(); + int index = getSceneIndex(scene); + if (index >= 0 && index < mSceneStack.size()) { + Pair pair = mSceneStack.remove(index); + Curtain curtain = pair.second; + Scene previousState = getTopState(); + + // Do animation via curtain + if (curtain != null) { + scene.destroy(false); + + if (previousState != null) { + curtain.open(previousState, scene); + } + } else { + scene.destroy(true); + } } else { Log.e(TAG, "The scene is not in stage"); } } + private int getSceneIndex(@NonNull Scene scene) { + int size = mSceneStack.size(); + while (--size >= 0) { + Pair pair = mSceneStack.get(size); + if (scene.equals(pair.first)) { + return size; + } + } + return -1; + } + boolean onBackPressed() { Scene scene = getTopState(); if (scene != null) { @@ -87,29 +119,32 @@ class SceneManager { private Scene getTopState() { if (!mSceneStack.isEmpty()) { - return mSceneStack.peek(); + return mSceneStack.peek().first; } else { return null; } } + /* private Scene getSecondTopState() { int secondTopIndex = mSceneStack.size() - 2; if (secondTopIndex >= 0) { - return mSceneStack.get(secondTopIndex); + return mSceneStack.get(secondTopIndex).first; } else { return null; } } + */ protected void onSaveInstanceState(Bundle outState) { - for (Scene scene : mSceneStack) { - scene.saveInstanceState(outState); + for (Pair p : mSceneStack) { + p.first.saveInstanceState(outState); } } protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { - for (Scene scene : mSceneStack) { + for (Pair p : mSceneStack) { + Scene scene = p.first; // Recreate scene.create(savedInstanceState); scene.resume(); diff --git a/app/src/main/java/com/hippo/scene/StageActivity.java b/app/src/main/java/com/hippo/scene/StageActivity.java index 95e3ac102..321bf5a5e 100644 --- a/app/src/main/java/com/hippo/scene/StageActivity.java +++ b/app/src/main/java/com/hippo/scene/StageActivity.java @@ -66,7 +66,7 @@ public abstract class StageActivity extends AbsActionBarActivity { } public void startScene(@NonNull Class sceneClass, @Nullable Announcer announcer) { - sSceneManager.startScene(sceneClass, announcer); + sSceneManager.startScene(sceneClass, announcer, null); } /** diff --git a/app/src/main/java/com/hippo/scene/TransitionCurtain.java b/app/src/main/java/com/hippo/scene/TransitionCurtain.java new file mode 100644 index 000000000..c1a1a0486 --- /dev/null +++ b/app/src/main/java/com/hippo/scene/TransitionCurtain.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2015 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.scene; + +import android.animation.ObjectAnimator; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; +import android.view.ViewTreeObserver; + +import com.hippo.util.ViewUtils; + +public class TransitionCurtain extends Curtain { + + private static long ANIMATE_TIME = 300L; + + private ViewPair[] mViewPairArray; + + public TransitionCurtain(ViewPair[] viewPairArray) { + mViewPairArray = viewPairArray; + } + + @Override + public void open(final @NonNull Scene enter, final @NonNull Scene exit) { + + for (ViewPair pair : mViewPairArray) { + final View enterView = pair.getToView(enter); + final View exitView = pair.getFromView(exit); + if (enterView == null || exitView == null) { + // Can't get to view + continue; + } + + // First make enter view invisible + ViewUtils.setVisibility(enterView, View.INVISIBLE); + + enterView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + ViewUtils.removeOnGlobalLayoutListener(enterView.getViewTreeObserver(), this); + + int[] startloaction = new int[2]; + ViewUtils.getLocationInAncestor(exitView, startloaction, exit.getStageActivity().getStageLayout()); + int[] endloaction = new int[2]; + ViewUtils.getLocationInAncestor(enterView, endloaction, enter.getStageActivity().getStageLayout()); + int startWidth = exitView.getWidth(); + int startHeight = exitView.getHeight(); + int endWidth = enterView.getWidth(); + int endHeight = enterView.getHeight(); + + ViewUtils.setVisibility(exitView, View.INVISIBLE); + ViewUtils.setVisibility(enterView, View.VISIBLE); + + // Start animation + ObjectAnimator scaleXAnim = ObjectAnimator.ofFloat(enterView, "scaleX", (float) startWidth / endWidth, 1f); + scaleXAnim.setDuration(ANIMATE_TIME); + ObjectAnimator scaleYAnim = ObjectAnimator.ofFloat(enterView, "scaleY", (float) startHeight / endHeight, 1f); + scaleYAnim.setDuration(ANIMATE_TIME); + ObjectAnimator xAnim = ObjectAnimator.ofFloat(enterView, "x", startloaction[0], endloaction[0]); + xAnim.setDuration(ANIMATE_TIME); + ObjectAnimator yAnim = ObjectAnimator.ofFloat(enterView, "y", startloaction[1], endloaction[1]); + yAnim.setDuration(ANIMATE_TIME); + + scaleXAnim.start(); + scaleYAnim.start(); + xAnim.start(); + yAnim.start(); + } + }); + } + } + + @Override + public void close(@NonNull Scene enter, @NonNull Scene exit) { + dispatchDetachFromeStage(exit); + } + + public static class ViewPair { + + private int[] mIds; + private GetViewHelper mHelper; + + public ViewPair(int from, int to) { + mIds = new int[]{from, to}; + } + + public ViewPair(@NonNull GetViewHelper helper) { + mHelper = helper; + } + + private @Nullable View getFromView(Scene scene) { + View sceneView = scene.getSceneView(); + + if (mIds != null && sceneView != null) { + return sceneView.findViewById(mIds[0]); + } + if (mHelper != null) { + return mHelper.getFromView(sceneView); + } + return null; + } + + private @Nullable View getToView(Scene scene) { + View sceneView = scene.getSceneView(); + + if (mIds != null && sceneView != null) { + return sceneView.findViewById(mIds[1]); + } + if (mHelper != null) { + return mHelper.getToView(sceneView); + } + return null; + } + } + + public abstract static class GetViewHelper { + + public abstract View getFromView(@Nullable View sceneView); + + public abstract View getToView(@Nullable View sceneView); + } +} diff --git a/app/src/main/java/com/hippo/util/ViewUtils.java b/app/src/main/java/com/hippo/util/ViewUtils.java index 9f7c45abe..c210afebb 100644 --- a/app/src/main/java/com/hippo/util/ViewUtils.java +++ b/app/src/main/java/com/hippo/util/ViewUtils.java @@ -22,6 +22,7 @@ import android.os.Build; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.ViewTreeObserver; public final class ViewUtils { @@ -44,6 +45,17 @@ public final class ViewUtils { * @param location an array of two integers in which to hold the coordinates */ public static void getLocationInWindow(View view, int[] location) { + getLocationInAncestor(view, location, android.R.id.content); + } + + /** + * Get view location in ths ancestor + * + * @param view + * @param location + * @param ancestorId + */ + public static void getLocationInAncestor(View view, int[] location, int ancestorId) { if (location == null || location.length < 2) { throw new IllegalArgumentException( "location must be an array of two integers"); @@ -57,7 +69,45 @@ public final class ViewUtils { ViewParent viewParent = view.getParent(); while (viewParent instanceof View) { view = (View) viewParent; - if (view.getId() == android.R.id.content) { + if (view.getId() == ancestorId) { + break; + } + + position[0] -= view.getScrollX(); + position[1] -= view.getScrollY(); + + position[0] += view.getLeft(); + position[1] += view.getTop(); + + viewParent = view.getParent(); + } + + location[0] = (int) (position[0] + 0.5f); + location[1] = (int) (position[1] + 0.5f); + } + + /** + * Get view location in ths ancestor + * + * @param view + * @param location + * @param ancestor + */ + public static void getLocationInAncestor(View view, int[] location, View ancestor) { + if (location == null || location.length < 2) { + throw new IllegalArgumentException( + "location must be an array of two integers"); + } + + float[] position = new float[2]; + + position[0] = view.getLeft(); + position[1] = view.getTop(); + + ViewParent viewParent = view.getParent(); + while (viewParent instanceof View) { + view = (View) viewParent; + if (viewParent == ancestor) { break; } @@ -281,4 +331,18 @@ public final class ViewUtils { return result; } + /** + * removeOnGlobalLayoutListener + * + * @param vto + * @param l + */ + public static void removeOnGlobalLayoutListener(ViewTreeObserver vto, + ViewTreeObserver.OnGlobalLayoutListener l) { + if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + vto.removeGlobalOnLayoutListener(l); + } else { + vto.removeOnGlobalLayoutListener(l); + } + } } diff --git a/app/src/main/res/layout/fragment_list.xml b/app/src/main/res/layout/fragment_list.xml index 918c97f4b..f0613fda1 100644 --- a/app/src/main/res/layout/fragment_list.xml +++ b/app/src/main/res/layout/fragment_list.xml @@ -19,24 +19,17 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + android:src="@drawable/ic_happy"/> - + android:text="Click" + android:id="@+id/test_button" + android:layout_gravity="center_horizontal|bottom"/> - diff --git a/app/src/main/res/layout/scene_test.xml b/app/src/main/res/layout/scene_test.xml new file mode 100644 index 000000000..ebb8a065a --- /dev/null +++ b/app/src/main/res/layout/scene_test.xml @@ -0,0 +1,30 @@ + + + + + + +