/* * Copyright (c) 2013 Menny Even-Danan * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.evendanan.pushingpixels; import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v7.app.ActionBarActivity; import android.view.View; import com.anysoftkeyboard.utils.Log; import com.menny.android.anysoftkeyboard.R; public abstract class FragmentChauffeurActivity extends ActionBarActivity { public static enum FragmentUiContext { RootFragment, DeeperExperience, ExpandedItem, IncomingAlert } private static final String TAG = "chauffeur"; private static final String ROOT_FRAGMENT_TAG = "FragmentChauffeurActivity_ROOT_FRAGMENT_TAG"; private static final String KEY_FRAGMENT_CLASS_TO_ADD = "KEY_FRAGMENT_CLASS_TO_ADD"; private static final String KEY_FRAGMENT_ARGS_TO_ADD = "KEY_FRAGMENT_ARGS_TO_ADD"; public static void addIntentArgsForAddingFragmentToUi(@NonNull Intent intent, @NonNull Class<? extends Fragment> fragmentClass, @Nullable Bundle fragmentArgs) { intent.putExtra(KEY_FRAGMENT_CLASS_TO_ADD, fragmentClass); if (fragmentArgs != null) intent.putExtra(KEY_FRAGMENT_ARGS_TO_ADD, fragmentArgs); } private boolean mIsActivityShown = false; @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); mIsActivityShown = true; if (savedInstanceState == null) { //setting up the root of the UI. setRootFragment(createRootFragmentInstance()); //now, checking if there is a request to add a fragment on-top of this one. Bundle activityArgs = getIntent().getExtras(); if (activityArgs != null && activityArgs.containsKey(KEY_FRAGMENT_CLASS_TO_ADD)) { Class<? extends Fragment> fragmentClass = (Class<? extends Fragment>) activityArgs.get(KEY_FRAGMENT_CLASS_TO_ADD); //not sure that this is a best-practice, but I still need to remove this from the activity's args activityArgs.remove(KEY_FRAGMENT_CLASS_TO_ADD); try { Fragment fragment = fragmentClass.newInstance(); if (activityArgs.containsKey(KEY_FRAGMENT_ARGS_TO_ADD)) { fragment.setArguments(activityArgs.getBundle(KEY_FRAGMENT_ARGS_TO_ADD)); activityArgs.remove(KEY_FRAGMENT_CLASS_TO_ADD); } addFragmentToUi(fragment, FragmentUiContext.RootFragment); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } protected abstract int getFragmentRootUiElementId(); protected abstract Fragment createRootFragmentInstance(); public void returnToRootFragment() { if (!mIsActivityShown) return; getSupportFragmentManager().popBackStackImmediate(ROOT_FRAGMENT_TAG, 0 /*don't pop the root*/); } public void setRootFragment(Fragment fragment) { getSupportFragmentManager().popBackStack(ROOT_FRAGMENT_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.setCustomAnimations(R.anim.ui_context_root_add_in, R.anim.ui_context_root_add_out, R.anim.ui_context_root_pop_in, R.anim.ui_context_root_pop_out); transaction.replace(getFragmentRootUiElementId(), fragment); //bookmarking, so I can return easily. transaction.addToBackStack(ROOT_FRAGMENT_TAG); transaction.commit(); } public void addFragmentToUi(@NonNull Fragment fragment, FragmentUiContext experience) { addFragmentToUi(fragment, experience, null); } /** * Adds the given fragment into the UI using the specified UI-context animation. * * @param fragment any generic Fragment. For the ExpandedItem animation it is best to use a PassengerFragment * @param experience * @param originateView a hint view which will be used to fine-tune the ExpandedItem animation */ public void addFragmentToUi(@NonNull Fragment fragment, FragmentUiContext experience, @Nullable View originateView) { if (!mIsActivityShown) return; if (experience == FragmentUiContext.RootFragment) { //in this case, I need to pop all the other fragments till the root. returnToRootFragment(); } FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); //note: the animation should be declared before the fragment replace call, so the transaction will know to which fragment change it should be associated with. switch (experience) { case RootFragment: transaction.setCustomAnimations(R.anim.ui_context_root_add_in, R.anim.ui_context_root_add_out, R.anim.ui_context_root_pop_in, R.anim.ui_context_root_pop_out); break; case DeeperExperience: transaction.setCustomAnimations(R.anim.ui_context_deeper_add_in, R.anim.ui_context_deeper_add_out, R.anim.ui_context_deeper_pop_in, R.anim.ui_context_deeper_pop_out); break; case ExpandedItem: //although this managing Activity can handle any generic Fragment, in this case we'll need some help from the fragment. //it is required to fine tune the pivot of the scale animation. //so, I'll need the specialized fragment PassengerFragment if (fragment instanceof Passengerable && originateView != null) { View fragmentParent = findViewById(getFragmentRootUiElementId()); // Idea taken from: // http://developer.android.com/training/animation/zoom.html final float scaleX = ((float) originateView.getWidth()) / ((float) fragmentParent.getWidth()); final float scaleY = ((float) originateView.getHeight()) / ((float) fragmentParent.getHeight()); // some preparations // the Y pivot is tricky, it should be the middle of the button, but in // the fragmentParent coordinates int[] originateLocation = new int[2]; originateView.getLocationInWindow(originateLocation); int[] parentLocation = new int[2]; fragmentParent.getLocationInWindow(parentLocation); final int pivotY = originateLocation[1] - parentLocation[1] + (originateView.getHeight() / 2); final int pivotX = originateLocation[0] - parentLocation[0] + (originateView.getWidth() / 2); Passengerable passengerFragment = (Passengerable) fragment; passengerFragment.setItemExpandExtraData(pivotX, pivotY, scaleX, scaleY); transaction.setCustomAnimations(R.anim.ui_context_expand_add_in, R.anim.ui_context_expand_add_out, R.anim.ui_context_expand_pop_in, R.anim.ui_context_expand_pop_out); } else { //using the default scale animation, no pivot changes can be done on a generic fragment. transaction.setCustomAnimations(R.anim.ui_context_expand_add_in_default, R.anim.ui_context_expand_add_out, R.anim.ui_context_expand_pop_in, R.anim.ui_context_expand_pop_out_default); } break; case IncomingAlert: transaction.setCustomAnimations(R.anim.ui_context_dialog_add_in, R.anim.ui_context_dialog_add_out, R.anim.ui_context_dialog_pop_in, R.anim.ui_context_dialog_pop_out); break; default: Log.wtf(TAG, "I don't know what is this UI experience type: " + experience); break; } //these two calls will make sure the back-button will switch to previous fragment transaction.replace(getFragmentRootUiElementId(), fragment); transaction.addToBackStack(null); transaction.commit(); } @Override public void onBackPressed() { super.onBackPressed(); if (getSupportFragmentManager().getBackStackEntryCount() == 0) { //the UI is empty. I can safely finish the activity finish(); } } @Override protected void onStart() { super.onStart(); mIsActivityShown = true; } @Override protected void onStop() { super.onStop(); mIsActivityShown = false; } public final boolean isChaufferActivityVisible() { return mIsActivityShown; } }