preface

Gesture return is a very easy thing for users to do. It’s supported by Apple natively, but it hasn’t been considered on Android until now, so it has to be done by App developers themselves, but it gives developers room to create. Recently, in the spare time of busy business development, we extracted the basic fragment management class from QMUIDemo as a new library, and added the function of gesture return. Now we have completed the initial version, and you can try it out if you are interested.

Implementation "com. Qmuiteam: arch: 0.0.1."Copy the code

Then use QMUIFragmentActivity and QMUIFragment to build the UI as the base class. You can refer to the QMUI_Android project for how to use it. This paper will introduce its implementation principle and several control interfaces.

The Activity gesture returns

At present, open source gesture return implementation is basically for the Activity, such as the classic implementation: SwipeBackLayout, the classic, because the implementation after basically use it to provide a View (SwipeBackLayout). The principle of returning an Activity gesture is also very simple: make the Activity transparent at the start of the drag so that you can see the Activity behind it. However, the system does not provide an interface to make the Activity transparent, so you can only do this through reflection. Of course, making an Activity transparent is performance-costly and can cause other potholes, so there are alternatives, such as and_swipeback. For the use of SwipeBackLayout and how to use reflection to make an Activity transparent, here is a recommended Android platform slide back library comparison.

Single Activity multiple Fragment gestures are returned.

I prefer the UI architecture of single Activity and multiple fragments: lightweight, more flexible, no need to change AndroidManifest every time a new interface is added, etc.

There are also gesture return implementations for fragments, but only if the fragments are added to the view one by one. This is not very elegant. If you have deep navigation, your view will have many fragments at the same time, which should become more and more prone to stalling. QMUIFragment adopt the way of the replace, such view can exist only a Fragment, guarantee performance, can have a look at QMUIFragmentActivity. StartFragment method:

public void startFragment(QMUIFragment fragment) {  
    QMUIFragment.TransitionConfig transitionConfig = fragment.onFetchTransitionConfig();
    String tagName = fragment.getClass().getSimpleName();
    getSupportFragmentManager()
            .beginTransaction()
            .setCustomAnimations(transitionConfig.enter, transitionConfig.exit, transitionConfig.popenter, transitionConfig.popout)
            .replace(getContextViewId(), fragment, tagName)
            .addToBackStack(tagName)
            .commit();
}
Copy the code

Replace method to achieve Fragment jump, the cost is that gesture return is very difficult to implement. Without a clear understanding of how FragmentManager and BackStackRecord work, it’s almost impossible to implement this feature. That’s why I was slow to add this feature, having spent a lot of time working out the implementation logic of FragmentManager up front.

What addToBackStack does is add a Fragment to the BackStack. It doesn’t, it adds an operation (Op). For example, the replace operation, which is two operations: a remove and an add, is logged by BackStackRcord, which performs the reverse operation based on the recorded operation when popBackStack. So a key point to implement the gesture return can be identified, modifying the actions recorded in BackStackRcord.

Let’s take a look at the action triggered by gesture return:

public void onEdgeTouch(int edgeFlag) { Log.i(TAG, "SwipeListener:onEdgeTouch: edgeFlag = " + edgeFlag); FragmentManager fragmentManager = getFragmentManager(); if (fragmentManager == null) { return; } int backstackCount = fragmentManager.getBackStackEntryCount(); BackStackRcord: Fragment if (backstackCount > 1) {try {BackStackRcord: Fragment (backstackCount > 1) { BackStackRcord is the only implementation class BackStackEntry FragmentManager. BackStackEntry BackStackEntry = fragmentManager.getBackStackEntryAt(backstackCount - 1); // Get the record of this operation by reflection: Field opsField = BackStackEntry.getClass ().getDeclaredField("mOps"); backStackEntry.getClass(). opsField.setAccessible(true); Object opsObj = opsField.get(backStackEntry); if (opsObj instanceof List<? >) { List<? > ops = (List<? >) opsObj; For (Object op: ops) {cmdField = op.getClass().getDeclaredField(" CMD "); cmdField.setAccessible(true); int cmd = (int) cmdField.get(op); If (CMD == 3) {// If (CMD == 3) {// If (CMD == 3); This way the gesture return will not trigger the entry animation of the previous fragment. Field popEnterAnimField = op.getClass().getDeclaredField("popEnterAnim"); popEnterAnimField.setAccessible(true); popEnterAnimField.set(op, 0); // Use the reflection fragment field to obtain the fragment that was previously removed. FragmentField = op.getClass().getDeclaredField("fragment"); fragmentField = op.getClass(). fragmentField.setAccessible(true); Object fragmentObject = fragmentField.get(op); if (fragmentObject instanceof QMUIFragment) { QMUIFragment fragment = (QMUIFragment) fragmentObject; // Add the View managed by the previous fragment to the lowest level of the View. Container = getBaseFragmentActivity().getFragmentContainer(); // Trigger the onCreateView of the previous fragment (3 arguments) to get the view managed by the fragment. fragment.isCreateForSwipeBack = true; View baseView = fragment.onCreateView(LayoutInflater.from(getContext()), container, null); fragment.isCreateForSwipeBack = false; if (baseView ! SetTag (r.i.d.qmui_ARCH_swipe_layout_in_back, SWIPE_BACK_VIEW); // Add it to the bottom layer of the view container.addView(baseView, 0); Int offset = math.abs (backViewInitOffset()); if (edgeFlag == EDGE_BOTTOM) { ViewCompat.offsetTopAndBottom(baseView, offset); } else if (edgeFlag == EDGE_RIGHT) { ViewCompat.offsetLeftAndRight(baseView, offset); } else { ViewCompat.offsetLeftAndRight(baseView, -1 * offset); } } } } } } } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); }} else {// If it is already the first fragment, return to the Activity gesture and change the Activity to transparent if (getActivity()! = null) { getActivity().getWindow().getDecorView().setBackgroundColor(0); Utils.convertActivityToTranslucent(getActivity()); }}}Copy the code

The main core is to remove the entry animation of the previous fragment and add the view it manages to the lower level of the view. In order to mimic the parallax effect of wechat, I also provide a method backInitOffset(), subclass override, can get perfect imitation of parallax scrolling, of course, if the activity is not supported.

During the drag-and-drop process, you basically update the position of the view behind you, without much content. And then the drag is done. Divided into two cases, one is to abandon the return, one is to perform the return. If the return is abandoned, delete the View behind it. If the return is performed, set the exit animation of the current fragment to 0 and popbackstack. The specific code is:

public void onScrollStateChange(int state, float scrollPercent) { ViewGroup container = getBaseFragmentActivity().getFragmentContainer(); int childCount = container.getChildCount(); If (state == swipebacklayout.state_idle) {if (scrollPercent <= 0.0f) { View for (int I = childCount - 1; i >= 0; i--) { View view = container.getChildAt(i); Object tag = view.getTag(R.id.qmui_arch_swipe_layout_in_back); if (tag ! = null && SWIPE_BACK_VIEW.equals(tag)) { container.removeView(view); }}} else if (scrollPercent >= 1.0f) {for (int I = childcount-1; i >= 0; i--) { View view = container.getChildAt(i); Object tag = view.getTag(R.id.qmui_arch_swipe_layout_in_back); if (tag ! = null && SWIPE_BACK_VIEW.equals(tag)) { container.removeView(view); } } FragmentManager fragmentManager = getFragmentManager(); if (fragmentManager == null) { return; } int backstackCount = fragmentManager.getBackStackEntryCount(); if (backstackCount > 0) { try { FragmentManager.BackStackEntry backStackEntry = fragmentManager.getBackStackEntryAt(backstackCount - 1); Field opsField = backStackEntry.getClass().getDeclaredField("mOps"); opsField.setAccessible(true); Object opsObj = opsField.get(backStackEntry); if (opsObj instanceof List<? >) { List<? > ops = (List<? >) opsObj; for (Object op : ops) { Field cmdField = op.getClass().getDeclaredField("cmd"); cmdField.setAccessible(true); int cmd = (int) cmdField.get(op); If (CMD == 1) {// If (CMD == 1) {// If (CMD == 1) {// If (CMD == 1) { We need to remove its remove animation Field popEnterAnimField = op.getClass().getDeclaredField("popExitAnim"); popEnterAnimField.setAccessible(true); popEnterAnimField.set(op, 0); } } } } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } popBackStack(); }}}Copy the code

So the whole gesture return flow is clear. There is another problem. The onCreateView(3 argument) of the previous fragment will be executed multiple times. The gesture return will fire once, and popBackStack will fire again, so we need to cache the View created by the Fragment. But you can’t just store it in a member variable. There are several scenarios to consider:

  1. The View is in the animation process, and sometimes we’ll get into a screen and we’ll go back quickly before the animation is finished, and that will trigger the View to remove and add the animation before the animation is finished, and the problem with that can be seen here

  2. With the Android Support package upgraded to 27, FragmentManager now supports Transition. However, if you use transition and animation at the same time, you will fall into a pit where the View cannot be removed successfully. I have submitted a list of bugs to Google. I hope they can fix it.

In view of these two points, my approach is as follows:

  1. Through reflection fragments. GetAnimatingAway (), determine whether it is in the process of animation, if it is, is abandoned to recreate the View, later see can find the better way
  2. If you fall into a pit where the view cannot be successfully removed, you will have a phenomenon:view.getParent ! = null && view.getParent.indexOfChild(view) == -1. So. If this condition is met, reflection forces the mParent to be null. Specific code:
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { SwipeBackLayout swipeBackLayout; if (mCacheView == null) { swipeBackLayout = newSwipeBackLayout(); mCacheView = swipeBackLayout; } else if (isCreateForSwipeBack) { // in swipe back, must not in animation swipeBackLayout = mCacheView; } else { boolean isInRemoving = false; try { Method method = Fragment.class.getDeclaredMethod("getAnimatingAway"); method.setAccessible(true); Object object = method.invoke(this); if (object ! = null) { isInRemoving = true; } } catch (NoSuchMethodException e) { isInRemoving = true; e.printStackTrace(); } catch (IllegalAccessException e) { isInRemoving = true; e.printStackTrace(); } catch (InvocationTargetException e) { isInRemoving = true; e.printStackTrace(); } if (isInRemoving) { swipeBackLayout = newSwipeBackLayout(); mCacheView = swipeBackLayout; } else { swipeBackLayout = mCacheView; } } if (! isCreateForSwipeBack) { mBaseView = swipeBackLayout.getContentView(); swipeBackLayout.setTag(R.id.qmui_arch_swipe_layout_in_back, null); } ViewCompat.setTranslationZ(swipeBackLayout, mBackStackIndex); swipeBackLayout.setFitsSystemWindows(false); if (getActivity() ! = null) { QMUIViewHelper.requestApplyInsets(getActivity().getWindow()); } if (swipeBackLayout.getParent() ! = null) { ViewGroup viewGroup = (ViewGroup) swipeBackLayout.getParent(); if (viewGroup.indexOfChild(swipeBackLayout) > -1) { viewGroup.removeView(swipeBackLayout); } else { // see https://issuetracker.google.com/issues/71879409 try { Field parentField = View.class.getDeclaredField("mParent"); parentField.setAccessible(true); parentField.set(swipeBackLayout, null); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } return swipeBackLayout; }Copy the code

Finally, QMUIFragment provides canDragBack, which controls whether the current fragment can gesture back.

This is the best version I can think of. Later may be through intensive reading of the source code, with many improvements. The main drawback of the current solution is the heavy use of reflection. If the support package is updated and some fields are changed, the gesture return may not work properly.