appcompat-v7 v23.0.0 statusbar color black when in ActionMode
Asked Answered
C

3

6

UPDATE

Same issue present in the latest Gmail app. I still don't understand why would Google make such unpleasant UI change. Obsessive in me goes crazy whenever I see it

QUESTION

I have this weird issue with appcompat-v7 23. Issue I am going to describe does not happen with 22 series

You can get source code that reproduces this issuse form https://github.com/devserv/t/ Once built, you can tap and hold an item in the list to activate ActionMode

Issue:

When in ActionMode, appcompat turns status bar to black. This does not happen if I don’t use following

<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>

in my v21 style but I have to use it because I want my navigation drawer to look behind status bar.


I used to use following to avoid black status bar when ActionMode started and ended

 public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        getActivity().getWindow().setStatusBarColor(getResources().getColor(R.color.appColorPrimaryDark));
    }

}

 public void onDestroyActionMode(ActionMode actionMode) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        getActivity().getWindow().setStatusBarColor(getResources().getColor(android.R.color.transparent));
     }

    mMode = null;
}

Above code did not create/avoided status bar turning black, but does not work properly on v23 of appcompat. Instead you see a short black status bar while ActionMode destroyed. It looks like related to the animation that plays when ActionMode destroyed.

I have tried to open bug reports but it has been declined with comment

Don't re-create bugs.

Am I missing something?

Here are the screenshots for normal and action mode.

enter image description here enter image description here

Carefree answered 31/8, 2015 at 19:29 Comment(3)
Have you tried with 23.0.1 release? It has been deployed today. What is the api level of your device?Pileus
Yes, still the same. They don't even accept the bug reportCarefree
This problem has been fixed with This has been fixed with com.android.support:design:28.0.0-rc01.Floribunda
U
9

In case only the color is the issue, you can change it. Only to a fixed color resource.

<color name="abc_input_method_navigation_guard" tools:override="true">@color/primary_dark</color>

Obvious ?colorPrimaryDark will not work, not even on API 21.


The view responsible for the black status bar background is stored in AppCompatDelegateImplV7.mStatusGuard. You can get the delegate by calling getDelegate() from your activity and access mStatusGuard field by reflection. After starting the action mode you can get a reference to this view and customize it however you like.

This was found in AppCompat 24.1.1.

Uncinate answered 1/8, 2016 at 15:42 Comment(1)
In androidx.appcompat 1.2.0 the corresponding color's name is abc_decor_view_status_guard.Keratoid
D
7

The version 23.0.0 of v7 appcompat library introduced an animation that fades in and out the action mode when it's started and finished as you can read here:

The action mode has fades in and is working as intended.

The changes are made in the method onDestroyActionMode in AppCompatDelegateImplV7:

public void onDestroyActionMode(ActionMode mode) {
    mWrapped.onDestroyActionMode(mode);
    if (mActionModePopup != null) {
        mWindow.getDecorView().removeCallbacks(mShowActionModePopup);
        mActionModePopup.dismiss();
    } else if (mActionModeView != null) {
        mActionModeView.setVisibility(View.GONE);
        if (mActionModeView.getParent() != null) {
            ViewCompat.requestApplyInsets((View) mActionModeView.getParent());
        }
    }
    if (mActionModeView != null) {
        mActionModeView.removeAllViews();
    }
    if (mAppCompatCallback != null) {
        mAppCompatCallback.onSupportActionModeFinished(mActionMode);
    }
    mActionMode = null;
}

In version 23.0.0 it was changed to:

public void onDestroyActionMode(ActionMode mode) {
    mWrapped.onDestroyActionMode(mode);
    if (mActionModePopup != null) {
        mWindow.getDecorView().removeCallbacks(mShowActionModePopup);
    }

    if (mActionModeView != null) {
        endOnGoingFadeAnimation();
        mFadeAnim = ViewCompat.animate(mActionModeView).alpha(0f);
        mFadeAnim.setListener(new ViewPropertyAnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(View view) {
                mActionModeView.setVisibility(View.GONE);
                if (mActionModePopup != null) {
                    mActionModePopup.dismiss();
                } else if (mActionModeView.getParent() instanceof View) {
                    ViewCompat.requestApplyInsets((View) mActionModeView.getParent());
                }
                mActionModeView.removeAllViews();
                mFadeAnim.setListener(null);
                mFadeAnim = null;
            }
        });
    }
    if (mAppCompatCallback != null) {
        mAppCompatCallback.onSupportActionModeFinished(mActionMode);
    }
    mActionMode = null;
}

As you can see mWrapped.onDestroyActionMode(mode); is called immediately, not when the animation ends. This is what cause the black status bar in your app and in other apps like Gmail and Keep.

The workaround that you found works, but unfortunately is not reliable, because if the animation takes longer you could see the black status bar anyway.

I think Google should correct the issue and call onDestroyActionMode only when the animation is really ended. In the mean time you can change this behaviour with a bit of reflections. It is necessary to override onSupportActionModeStarted in your activity and call the method fixActionModeCallback:

@Override
public void onSupportActionModeStarted(ActionMode mode) {
    super.onSupportActionModeStarted(mode);

    //Call this method
    fixActionModeCallback(this, mode);
}

private void fixActionModeCallback(AppCompatActivity activity, ActionMode mode) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
        return;

    if (!(mode instanceof StandaloneActionMode))
        return;

    try {
        final Field mCallbackField = mode.getClass().getDeclaredField("mCallback");
        mCallbackField.setAccessible(true);
        final Object mCallback = mCallbackField.get(mode);

        final Field mWrappedField = mCallback.getClass().getDeclaredField("mWrapped");
        mWrappedField.setAccessible(true);
        final ActionMode.Callback mWrapped = (ActionMode.Callback) mWrappedField.get(mCallback);

        final Field mDelegateField = AppCompatActivity.class.getDeclaredField("mDelegate");
        mDelegateField.setAccessible(true);
        final Object mDelegate = mDelegateField.get(activity);

        mCallbackField.set(mode, new ActionMode.Callback() {

            @Override
            public boolean onCreateActionMode(android.support.v7.view.ActionMode mode, Menu menu) {
                return mWrapped.onCreateActionMode(mode, menu);
            }

            @Override
            public boolean onPrepareActionMode(android.support.v7.view.ActionMode mode, Menu menu) {
                return mWrapped.onPrepareActionMode(mode, menu);
            }

            @Override
            public boolean onActionItemClicked(android.support.v7.view.ActionMode mode, MenuItem item) {
                return mWrapped.onActionItemClicked(mode, item);
            }

            @Override
            public void onDestroyActionMode(final android.support.v7.view.ActionMode mode) {
                Class mDelegateClass = mDelegate.getClass().getSuperclass();
                Window mWindow = null;
                PopupWindow mActionModePopup = null;
                Runnable mShowActionModePopup = null;
                ActionBarContextView mActionModeView = null;
                AppCompatCallback mAppCompatCallback = null;
                ViewPropertyAnimatorCompat mFadeAnim = null;
                android.support.v7.view.ActionMode mActionMode = null;

                Field mFadeAnimField = null;
                Field mActionModeField = null;

                while (mDelegateClass != null) {
                    try {
                        if (TextUtils.equals("AppCompatDelegateImplV7", mDelegateClass.getSimpleName())) {
                            Field mActionModePopupField = mDelegateClass.getDeclaredField("mActionModePopup");
                            mActionModePopupField.setAccessible(true);
                            mActionModePopup = (PopupWindow) mActionModePopupField.get(mDelegate);

                            Field mShowActionModePopupField = mDelegateClass.getDeclaredField("mShowActionModePopup");
                            mShowActionModePopupField.setAccessible(true);
                            mShowActionModePopup = (Runnable) mShowActionModePopupField.get(mDelegate);

                            Field mActionModeViewField = mDelegateClass.getDeclaredField("mActionModeView");
                            mActionModeViewField.setAccessible(true);
                            mActionModeView = (ActionBarContextView) mActionModeViewField.get(mDelegate);

                            mFadeAnimField = mDelegateClass.getDeclaredField("mFadeAnim");
                            mFadeAnimField.setAccessible(true);
                            mFadeAnim = (ViewPropertyAnimatorCompat) mFadeAnimField.get(mDelegate);

                            mActionModeField = mDelegateClass.getDeclaredField("mActionMode");
                            mActionModeField.setAccessible(true);
                            mActionMode = (android.support.v7.view.ActionMode) mActionModeField.get(mDelegate);

                        } else if (TextUtils.equals("AppCompatDelegateImplBase", mDelegateClass.getSimpleName())) {
                            Field mAppCompatCallbackField = mDelegateClass.getDeclaredField("mAppCompatCallback");
                            mAppCompatCallbackField.setAccessible(true);
                            mAppCompatCallback = (AppCompatCallback) mAppCompatCallbackField.get(mDelegate);

                            Field mWindowField = mDelegateClass.getDeclaredField("mWindow");
                            mWindowField.setAccessible(true);
                            mWindow = (Window) mWindowField.get(mDelegate);
                        }

                        mDelegateClass = mDelegateClass.getSuperclass();
                    } catch (NoSuchFieldException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }

                if (mActionModePopup != null) {
                    mWindow.getDecorView().removeCallbacks(mShowActionModePopup);
                }

                if (mActionModeView != null) {
                    if (mFadeAnim != null) {
                        mFadeAnim.cancel();
                    }

                    mFadeAnim = ViewCompat.animate(mActionModeView).alpha(0.0F);

                    final PopupWindow mActionModePopupFinal = mActionModePopup;
                    final ActionBarContextView mActionModeViewFinal = mActionModeView;
                    final ViewPropertyAnimatorCompat mFadeAnimFinal = mFadeAnim;
                    final AppCompatCallback mAppCompatCallbackFinal = mAppCompatCallback;
                    final android.support.v7.view.ActionMode mActionModeFinal = mActionMode;
                    final Field mFadeAnimFieldFinal = mFadeAnimField;
                    final Field mActionModeFieldFinal = mActionModeField;

                    mFadeAnim.setListener(new ViewPropertyAnimatorListenerAdapter() {
                        public void onAnimationEnd(View view) {
                            mActionModeViewFinal.setVisibility(View.GONE);
                            if (mActionModePopupFinal != null) {
                                mActionModePopupFinal.dismiss();
                            } else if (mActionModeViewFinal.getParent() instanceof View) {
                                ViewCompat.requestApplyInsets((View) mActionModeViewFinal.getParent());
                            }

                            mActionModeViewFinal.removeAllViews();
                            mFadeAnimFinal.setListener((ViewPropertyAnimatorListener) null);

                            try {
                                if (mFadeAnimFieldFinal != null) {
                                    mFadeAnimFieldFinal.set(mDelegate, null);
                                }
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            }

                            mWrapped.onDestroyActionMode(mode);

                            if (mAppCompatCallbackFinal != null) {
                                mAppCompatCallbackFinal.onSupportActionModeFinished(mActionModeFinal);
                            }

                            try {
                                if (mActionModeFieldFinal != null) {
                                    mActionModeFieldFinal.set(mDelegate, null);
                                }
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                }
            }
        });

    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
}
Debonair answered 6/9, 2015 at 9:18 Comment(4)
I was sure animation causing it, (see my replies below) your reply confirm this. What I don't understand is what they are insisting that it is working as intended. It clearly doesn't.Carefree
I think they didn't clearly understand the issue. The issue is not the animation itself (that obviously work as intended) but the fact that onDestroyActionMode is called too soon.Debonair
Hopefully @chris-bane is aware of itCarefree
I thought I will not implement it. I have accepted your answer as it explains cleary what causing this. Thanks for your time and effortCarefree
C
1

Nobody? Here is the workaround I came up with. Delay.

@Override
    public void onDestroyActionMode(ActionMode mode) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    try {
                        getActivity().getWindow().setStatusBarColor(getResources().getColor(android.R.color.transparent));
                    }
                    catch(Exception e)
                    {
                        e.printStackTrace();
                    }
                }
            }, 400);

        }
        mActionMode = null;

    }
Carefree answered 1/9, 2015 at 21:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.