SLWidget/SwipeBack Demo Experience: SLWidget (1.5MB)

Lateral spreads Screen rotation Window mode

nonsense

The iPhone6Plus, which had been used for more than three years, was recently replaced with a Samsung S9+. Smooth chicken eating experience, silky screen, super cost-effective (Hong Kong bank also hit another 10% discount), really like not. However, switching from IOS to Android is still not a good fit, the first is not! There are! Side! Slide! To return! Back!!! Every day ant forest stole energy points countless times the return key, simply crash! Therefore, the hot (love) love (love) work (load) make (force) I decided to have love in their own projects can not slide function.

Analysis of the

Search “Android Slide-back” and there are many, many open source libraries to choose from. I tried almost every type and found many, many holes. According to the different implementation methods, I roughly grouped them into two categories:

  • Opaque scheme

    Opaque plan management Activity stack ActivityLifecycleCallbacks callbacks through registration, in order to get the lower Activity of ContentView, then drawn in the upper Activity.

    • Opaque scheme branch one

      Insert a Layout into the top-level Activity’s DecorView. Listen for the sideslipping event to move the top-level Activity’s ContentView while calling View.draw(Canvas Canvas) from that Layout’s onDraw to draw the ContentView of the lower-level Activity. Create the illusion of sideslip into the underlying Activity. Problem: When the layout changes or data is updated, such as vertical and horizontal switching, hidden navigation bar, window mode, split screen mode, etc., the illusion remains unchanged.

    • Opaque scheme branch two

      Insert a Layout into the top-level Activity’s DecorView. Remove the ContentView from the underlying Activity and add it to this Layout. Listening for sideslip events and moving the top-level Activity’s ContentView can also give the illusion that the sideslip is visible to the lower-level Activity. This scheme is better than scheme 1: it can accommodate some layout changes. Problem: Data changes in the underlying Activity, no corresponding update. When the top-level Activity rebuilds (rotates the screen, switches window modes, and so on), the data bound in the ContentView is lost. If the underlying Activity has two layouts when you rotate the screen, this is misleading.

  • Transparent solution

    By setting window transparency, you can really see the interface of the underlying Activity.

    • Transparency Scheme 1

      Configure the following two properties in styles:

      <item name="android:windowBackground">@android:color/transparent</item>
      <item name="android:windowIsTranslucent">true</item>
      Copy the code

      Then listen for the sideslip event and move the top-level Activity’s ContentView to really see the interface of the lower-level Activity. At this point, no matter the layout changes, data updates, no problem. BUT!!! The scheme is a million problems… Problems: When windowIsTranslucent is true, a series of animation problems can be caused, such as switching between front and back animation, Activity backoff animation, etc. Online said there is a solution set “android: windowEnterAnimation” and “android: windowExitAnimation”, after the test with no eggs. At the same time, in SDK26(Android8.0) and above, it will clash with the fixed screen orientation and cause flash back. At the same time, the underlying Activity will only enter the onPause state, but not onStop state, which will definitely crash you when the page is opened too much.

    • Transparency Scheme 2

      For transparency 1, configure the same two properties in Styles, use reflection to make the window opaque in onPause, and use reflection to make the window transparent in onResume. It seems to have solved the performance problem caused by the Activity not onStop in the lower layer. BUT!!! The problems with the scheme remain dire… There are problems: because the top layer of the Activity is transparent, the lower layer of the Activity is rebuilt when you rotate the screen, and then the window becomes transparent in onResume, and the lower layer of the Activity is resurrected as well… It’s a chain reaction. It’s scary! Meanwhile, a series of animation problems caused by windowIsTranslucent becoming true remained unresolved.

implementation

As you can see above, the window must be transparent for the underlying Activity to receive layout changes and data updates in order for the sideslip to not be an illusion. But window transparency affects animation and conflicts with screen rotation. Is it possible to keep the window transparent only during sideslip? Ofcourse we can use reflection to make the window transparent when the sideslip is triggered and opaque at the end of the sideslip. This allows you to see the underlying Activity while swiping, without conflicting with the screen rotation or affecting the use of animation. The principle is very simple, so let’s implement it step by step.

Note: Some students asked that the use of non-SDK interfaces is prohibited in Android P, but the interface of window transparent conversion belongs to the grey list and is not restricted at present.

  • Step.1 Status bar transparent

    In order to achieve sidesaddle return, the status bar must be eliminated to achieve an immersive experience. There’s not much BB here, just go to the code.

    private boolean setStatusBarTransparent(boolean darkStatusBar) {
         // If the SDK is greater than or equal to 24, check whether the window mode is used
        boolean isInMultiWindowMode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && mSwipeBackActivity.isInMultiWindowMode();
        // Window mode or SDK < 19, do not set status bar transparency
        if (isInMultiWindowMode || Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
            return false;
        } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            / / SDK less than 21
            mSwipeBackActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        } else {
            // THE SDK is greater than or equal to 21
            int systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
             // THE SDK is greater than or equal to 23
            if (darkStatusBar && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                // Set the status bar text & icon dark color
                systemUiVisibility |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
            }
            // Remove the status bar background
            mDecorView.setSystemUiVisibility(systemUiVisibility);
            // Make the status bar transparent
            mSwipeBackActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
            mSwipeBackActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
            mSwipeBackActivity.getWindow().setStatusBarColor(Color.TRANSPARENT);
        }
        // Listen for layout changes in the DecorView
        mDecorView.addOnLayoutChangeListener(mPrivateListener);
        return true;
    }
    Copy the code

    There are a few caveats here. I. SDK less than 19 does not support status bar transparency, and SD21 and above also have different implementation methods. II. SD23 and above support status bar color inversion. III. SD24 and above support window mode, here to judge, when the window mode, do not set the status bar transparent. IV. After the status bar is set transparently, the adjustResize of the input method becomes invalid. Network transmission solution android: fitsSystemWindows = “true” is not recommended, because it can lead to can’t draw under the status bar. So here we listen for DecorView layout changes that dynamically adjust the height of the child View to the visible part of the DecorView. Post the code:

        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
            // Get the visible area of the DecorView
            Rect visibleDisplayRect = new Rect();
            mDecorView.getWindowVisibleDisplayFrame(visibleDisplayRect);
            /** omit a little bit of code here. */ is mentioned later
            // When the status bar is transparent, the adjustResize of the input method does not take effect, so manually adjust the View height to fit
            if (isStatusBarTransparent()) {
                for (int i = 0; i < mDecorView.getChildCount(); i++) {
                    View child = mDecorView.getChildAt(i);
                    if (child instanceof ViewGroup) {
                        // Get the child ViewGroup of the DecorView
                        ViewGroup.LayoutParams childLp = child.getLayoutParams();
                        // Adjust the paddingBottom subviewGroup
                        int paddingBottom = bottom - visibleDisplayRect.bottom;
                        if (childLp instanceof ViewGroup.MarginLayoutParams) {
                            // Subtract bottomMargin to take into account the height of the navigation bar
                            paddingBottom -= ((ViewGroup.MarginLayoutParams) childLp).bottomMargin;
                        }
                        paddingBottom = Math.max(0, paddingBottom);
                        if(paddingBottom ! = child.getPaddingBottom()) {// Adjust the subviewGroup's paddingBottom to make the entire ViewGroup visible
                            child.setPadding(child.getPaddingLeft(), child.getPaddingTop(), child.getPaddingRight(), paddingBottom);
                        }
                        break; }}}}Copy the code

    There are also two small points to note here: the paddingBottom calculation must take into account the navigation bar height calculation. And paddingBottom can’t be negative.

  • Step.2 supports sideslip

    Now that the status bar is transparent, the next step is to make our interface slide-able. We implement this in the Activity’s dispatchTouchEvent method. First, in the ACTION_DOWN event of dispatchTouchEvent determine whether the pressed area is side and mark it. The direction of movement is then determined and marked in the ACTION_MOVE event of dispatchTouchEvent. In the case of horizontal scrolling, setTranslationX is called to the parent container of the ContentView to set the offset value and make the interface move. Why is it the parent container of the ContentView? Because ContentView does not contain ActionBar, although ActionBar is not recommended… Finally, in the ACTION_UP event of dispatchTouchEvent, determine the distance and determine whether to finish the current page based on the final speed and displacement. That’s the basic idea of making the page slide. BUT, this also involves multi-touch, Touch event cancellation of child View, end speed calculation, animation processing after release and so on. Limited to this piece of code is a little bit too much is not the point, I will not post it here. Those interested in details please read the source code

  • Step.3 Window transparency

    At this point, many students may ask why I slide the bottom is dark. Don’t worry, because we’re not out of the way yet. As mentioned, we need to use reflection to make the window transparent when the sideslip is triggered, and to make the window opaque when the sideslip is over. The last step explained how to make the page slide, so the rest takes care of itself. Please look at the code:

    // Make the window transparent
    private void convertToTranslucent(Activity activity) {
        if (activity.isTaskRoot()) return;// The Activity at the bottom of the stack is not handled
        isTranslucentComplete = false;// Complete conversion flag
        try {
            // Get the class object of the transparent conversion callback class
            if (mTranslucentConversionListenerClass == null) {
                Class[] clazzArray = Activity.class.getDeclaredClasses();
                for (Class clazz : clazzArray) {
                    if (clazz.getSimpleName().contains("TranslucentConversionListener")) { mTranslucentConversionListenerClass = clazz; }}}// Proxy transparent conversion callback
            if (mTranslucentConversionListener == null&& mTranslucentConversionListenerClass ! =null) {
                InvocationHandler invocationHandler = new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        isTranslucentComplete = true;
                        return null; }}; mTranslucentConversionListener = Proxy.newProxyInstance(mTranslucentConversionListenerClass.getClassLoader(),new Class[]{mTranslucentConversionListenerClass}, invocationHandler);
            }
            // Use reflection to make the window transparent. Note that SDK21 and above parameters differ
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                Object options = null;
                try {
                    Method getActivityOptions = Activity.class.getDeclaredMethod("getActivityOptions");
                    getActivityOptions.setAccessible(true);
                    options = getActivityOptions.invoke(this);
                } catch (Exception ignored) {
                }
                Method convertToTranslucent = Activity.class.getDeclaredMethod("convertToTranslucent", mTranslucentConversionListenerClass, ActivityOptions.class);
                convertToTranslucent.setAccessible(true);
                convertToTranslucent.invoke(activity, mTranslucentConversionListener, options);
            } else {
                Method convertToTranslucent = Activity.class.getDeclaredMethod("convertToTranslucent", mTranslucentConversionListenerClass);
                convertToTranslucent.setAccessible(true); convertToTranslucent.invoke(activity, mTranslucentConversionListener); }}catch (Throwable ignored) {
            isTranslucentComplete = true;
        }
        if (mTranslucentConversionListener == null) {
            isTranslucentComplete = true;
        }
        // Remove the window background
        mSwipeBackActivity.getWindow().setBackgroundDrawable(null);
    }
    Copy the code
    // Make the window opaque
    private void convertFromTranslucent(Activity activity) {
        if (activity.isTaskRoot()) return;// The Activity at the bottom of the stack is not handled
        try {
            Method convertFromTranslucent = Activity.class.getDeclaredMethod("convertFromTranslucent");
            convertFromTranslucent.setAccessible(true);
            convertFromTranslucent.invoke(activity);
        } catch (Throwable t) {
        }
    }
    Copy the code

    The code is a little long, but it’s easy to understand. Converttoalways takes a transparent conversion callback, proxy the transparent conversion callback, and reflection always turns the window clear. Convertfromalways is not further explained. Simply calling ConvertToalways before the side slip makes the window clear, or convertFromalways after releasing it. You will notice that there is a completed conversion flag, which will be explained later.

  • Step.4 Bottom shadow

    At this point, you’ve basically done sideslip and come back, so you can do it in three steps. But some students may feel that no shadow is not good-looking ah! This is easy, let’s create a custom ShadowView and then call setTranslationX while sideslip.

    public View getShadowView(ViewGroup swipeBackView) {
        if (mShadowView == null) {
            mShadowView = new ShadowView(mSwipeBackActivity);
            mShadowView.setTranslationX(-swipeBackView.getWidth());
            ((ViewGroup) swipeBackView.getParent()).addView(mShadowView, 0.new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        }
        return mShadowView;
    }
    Copy the code

    Insert the ShadowView into the parent of swipeBackView, which is the ContentView parent mentioned in Step.2. Maybe no one noticed that the getShadowView method is public, because I thought, maybe some people don’t like the fact that I have to see a Pikachu when I sideslip. You tell me… In addition to this step will have to say, but all those who have a few people to use the slide back library, all support wechat as the lower Activity linkage, here to point out, we (it) are (is) not (lazy) support (cancer) hold (complex) (hair).

Matters needing attention

After the above simple four steps, basically the effect has been great. But there are still some places that need special attention, as well as the front of the two pits, now backfill.

  • Tips.1

    Fill in the hole where the layout changes in the DecorView listen. When the layout changes, we achieve adjustResize for the input method by adjusting the paddingBottom of the DecorView child View. This leads to a problem. The input method pops up with a bottom-up animation, and during the animation period, this area of the window is colored black. This is intolerable to perfectionists, and our solution is to block up the dark block with a new View. Isn’t it a little old-fashioned… But it worked… Paste the missing code from the previous onLayoutChange block:

            mWindowBackGroundView = getWindowBackGroundView(mDecorView);
            if(mWindowBackGroundView ! =null) {
                // Block up the dark part
                mWindowBackGroundView.setTranslationY(visibleDisplayRect.bottom);
            }
    Copy the code
  • Tips.2

    In the transparency process of the front window, there is also a hole: the transparency conversion completion flag isTranslucentComplete. Why do you want this? Since it takes about 100ms to make the window transparent, if you move the ContentView before the conversion is complete, you’ll see black again underneath… This is certainly not what I want, so before moving it, judge that if the window has not become transparent, do not process it

    private void swipeBackEvent(int translation) {
        if(! isTranslucentComplete)return;
        if(mShadowView.getBackground() ! =null) {
            int alpha = (int) ((1F - 1F * translation / mShadowView.getWidth()) * 255);
            alpha = Math.max(0, Math.min(255, alpha));
            mShadowView.getBackground().setAlpha(alpha);
        }
        mShadowView.setTranslationX(translation - mShadowView.getWidth());
        mSwipeBackView.setTranslationX(translation);
    }
    Copy the code

    And as some of you might say, I don’t do it until the transformation is done, but when the transformation is done, it’s going to jump. Let’s say you jump from 0 to 100. The idea is very rigorous, but because of the window conversion of 100ms or so, unless the hand speed is very fast, otherwise there is not much distance, the basic can not see. If your hand is moving really fast, it’s changing so fast that you can’t really see whether it’s a gradient or a mutation. So that’s fine…

  • Tips.3

    After the side slide is released, you either return to the left origin or continue sliding to the right boundary and finish the Activity. As mentioned above, you need to make the window opaque after sliding. Note that if you finish the Activity, do not make the window opaque. Because the underlying Activity is transparent at this point, if it turns opaque, then finish the top-level Activity and the black window will flash. In addition, cancel the Activity’s exit animation after Finish.

        public void onAnimationEnd(Animator animation) {
            if(! isAnimationCancel) {// End the current Activity by finally moving beyond the half-width position
                if (mShadowView.getWidth() + 2 * mShadowView.getTranslationX() >= 0) {
                    mShadowView.setVisibility(View.GONE);
                    mSwipeBackActivity.finish();
                    mSwipeBackActivity.overridePendingTransition(-1, -1);// Cancel the return animation
                } else {
                    mShadowView.setTranslationX(-mShadowView.getWidth());
                    mSwipeBackView.setTranslationX(0); convertFromTranslucent(mSwipeBackActivity); }}}Copy the code
  • Tips.4

    The core principle of sideslip is to use reflection to transform window transparency. As mentioned in the previous exploration of transparency scheme, window transparency will affect the life cycle of the underlying Activity. When we make the window transparent, the lower level Activity will wake up and enter the onStart state, and if the screen rotates, the lower level Activity will be rebuilt. When we restore the window to opaque, the underlying Activity will go back to the onStop state. So if the logic of your Activity code is confusing, be sure to optimize the logic before using it.

  • Tips.5

    Sideslip will fail if the orientation of the top layer Activity is not the same as that of the lower layer Activity (except that the lower layer orientation is not locked). Please disable sideslip of this layer Activity. Example Scenario: Portrait Screen Tap a video to play it in landscape screen. This is easy to understand, for example, the top Activity landscape, the bottom locked portrait, when the side slide, whether the window is landscape or portrait, It’s a question…

  • Tips.6

    Because the status bar is transparent and the layout is drawn from the top of the screen, the Toolbar needs to add a paddingTop for the height of the status bar

    // Get the height of the status bar
    public int getStatusBarHeight(a) {
        int resourceId = getResources().getIdentifier("status_bar_height"."dimen"."android");
        try {
            return getResources().getDimensionPixelSize(resourceId);
        } catch (Resources.NotFoundException e) {
            return 0; }}Copy the code
  • Tips.7

    If you want to dynamically support vertical/horizontal switching (for example, there is a “support landscape” switch in APP), the screen direction should be specified as Behind follows the direction of the Activity at the bottom of the stack. At the same time, judge in onCreate. If it does not support vertical/horizontal switching, the screen direction will be locked (because it is invalid in SDK21 after testing).

  • Tips.8

    As many of you will notice, the “Android :windowBackground” property in Styles is broken because the background is removed to perspective into the underlying Activity. See the last line of the Converttoalways method:

    private void convertToTranslucent(Activity activity) {
        if (activity.isTaskRoot()) return; .// Remove the window background
        mSwipeBackActivity.getWindow().setBackgroundDrawable(null);
    }
    Copy the code

    Of course, activities at the bottom of the stack and those that do not slide are not affected. In the SDK21 (below Android5.0) must specify < item name = “android: windowIsTranslucent” > true < / item >, because in SDK21 Android5.0), of the following The ConvertToalways method called by reflection always turns only convertFromalways into transparent, not an inherently opaque window.

END

He rambled on and on, all in long paragraphs. Limited by personal ability, it is hard to avoid some negligence mistakes, welcome correction. If there is a better idea, please do not hesitate to give advice, this article is right to throw a brick to attract jade.

SLWidget/SwipeBack (1.5MB)

Finally, thanks for the following blog posts, I benefit a lot (some omissions, please forgive me)

Namely wait forever | Android sliding back (SlideBack for Android) HolenZhou | Android version and WeChat Activity slide backward effect identical SwipeBackLayout Ziv_xiao | Android right slide out + immersion (transparent) the status bar Hoist the love | imitation WeChat slide back, realize the background linkage (a, principle)