Android 从源码分析View层次之ActionMode

转载请注明出处:(http://blog.csdn.net/qq_35071078/article/details/72859905

我们都知道,android最上层的view是一个DecorView,它下面又会有多种不同的ViewGroup(具体是一个什么样子的View,那是activity根据我们定义的属性来自动加载布局的),而我们写的布局就会加载到DecorView的某个子View下。刚好最近闲着,准备自己看一看android的view层次,于是乎,我用递归打印出了一个activity中的View层次。发现DecorView下总是会有一个ViewStub,

这个是用递归算法打印出来的
这里写图片描述

第一反应这个ViewStub应该是加载ActionBar的,但是发现ActionBar有它自己的布局,那么这个ViewStub到底是干嘛的?在翻阅各大论坛和博客以及stackoverflow之后,终于知道这里的ViewStub是干啥的了,所以写一篇博客总结一下。
大家如果想了解view是怎么加载出来的可以看看泓洋大神的 Android 源码解析 之 setContentView写的很清晰的

ActionMode 是什么?

官方的解释是:表示用户界面的上下文模式。动作模式可用于提供替代交互模式和取代正常的UI部件直到完成。良好的动作模式的实例包括文本选择和上下文动作。
通俗点讲,它就是android里一种menu的方式,方式有很多,其他的我就不说了,我只说ActionMode,ActionMode是临时占据了ActionBar的位置的一个menu。这样就知道这个ViewStub是干啥的了,就是用来加载我们的菜单的。

ActionMode 怎么用?

只需要在activity里调用 startActionMode(Callback callback)这个方法就可以了。
例如我这里实例化了一个Callback:

private ActionMode.Callback mCallback = new ActionMode.Callback() {

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            return false;
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {
            // TODO Auto-generated method stub
        }

        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            MenuInflater inflater = mode.getMenuInflater();
            inflater.inflate(R.menu.actionmode, menu);
            return true;
        }

        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            boolean ret = false;
            if (item.getItemId() == R.id.actionmode_cancel) {
                mode.finish();
                ret = true;
            }
            return ret;
        }
    };

然后在需要的地方调用:

startActionMode(mCallback)

就ok了,activity会在适当的时候回调一个方法:

    @Nullable
    @Override
    public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) {
        return super.onWindowStartingActionMode(callback);
    }

类似这样:
这里写图片描述

startActionMode的实现过程

首先,我是在API 23上进行分析的,而且我查看的源码不是supportv7下的源码。就是普通的activity。关于如何查看源码,大家可以查看这篇博客源码查看方法
我们进入到activity中的startActionMode这个方法:

    @Nullable
    public ActionMode startActionMode(ActionMode.Callback callback) {
        return mWindow.getDecorView().startActionMode(callback);
    }

发现它是调用了DecorView的startActionMode,我们进入到这个方法:

@Override
    public ActionMode startActionModeForChild(View originalView,
            ActionMode.Callback callback) {
        return startActionModeForChild(originalView, callback, ActionMode.TYPE_PRIMARY);
    }
    @Override
    public ActionMode startActionModeForChild(
            View child, ActionMode.Callback callback, int type) {
        return startActionMode(child, callback, type);
    }
    @Override
    public ActionMode startActionMode(ActionMode.Callback callback) {
        return startActionMode(callback, ActionMode.TYPE_PRIMARY);
    }
    @Override
    public ActionMode startActionMode(ActionMode.Callback callback, int type) {
        return startActionMode(this, callback, type);
    }
    private ActionMode startActionMode(
            View originatingView, ActionMode.Callback callback, int type) {
            //最终会调用这个方法
        ActionMode.Callback2 wrappedCallback = new ActionModeCallback2Wrapper(callback);
        ActionMode mode = null;
        if (mWindow.getCallback() != null && !mWindow.isDestroyed()) {
            try {
                mode = mWindow.getCallback().onWindowStartingActionMode(wrappedCallback, type);
            } catch (AbstractMethodError ame) {
                // Older apps might not implement the typed version of this method.
                if (type == ActionMode.TYPE_PRIMARY) {
                    try {
                        mode = mWindow.getCallback().onWindowStartingActionMode(
                                wrappedCallback);
                    } catch (AbstractMethodError ame2) {
                        // Older apps might not implement this callback method at all.
                    }
                }
            }
        }
        if (mode != null) {
            if (mode.getType() == ActionMode.TYPE_PRIMARY) {
                cleanupPrimaryActionMode();
                mPrimaryActionMode = mode;
            } else if (mode.getType() == ActionMode.TYPE_FLOATING) {
                if (mFloatingActionMode != null) {
                    mFloatingActionMode.finish();
                }
                mFloatingActionMode = mode;
            }
        } else {
            mode = createActionMode(type, wrappedCallback, originatingView);
            if (mode != null && wrappedCallback.onCreateActionMode(mode, mode.getMenu())) {
                setHandledActionMode(mode);
            } else {
                mode = null;
            }
        }
        if (mode != null && mWindow.getCallback() != null && !mWindow.isDestroyed()) {
            try {
                mWindow.getCallback().onActionModeStarted(mode);
            } catch (AbstractMethodError ame) {
                // Older apps might not implement this callback method.
            }
        }
        return mode;
    }

其他的不用管,看这一句:

mode = mWindow.getCallback().onWindowStartingActionMode(wrappedCallback, type);

这里的mWindow.getCallback()实际上就是activity,因为activity实现了这个接口,所以这个地方就会执行到了前面说的activity里面的onWindowStartingActionMode(wrappedCallback, type)。那么再在activity中来查看这个方法:

    @Nullable
    @Override
    public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) {
        // Only Primary ActionModes are represented in the ActionBar.
        if (mActionModeTypeStarting == ActionMode.TYPE_PRIMARY) {
            initWindowDecorActionBar();
            if (mActionBar != null) {
                return mActionBar.startActionMode(callback);
            }
        }
        return null;
    }

这里有 initWindowDecorActionBar(),看名字就知道大致的意思了,是用来初始化DecorActionBar的。

    private void initWindowDecorActionBar() {
        Window window = getWindow();

        // Initializing the window decor can change window feature flags.
        // Make sure that we have the correct set before performing the test below.
        window.getDecorView();

        if (isChild() || !window.hasFeature(Window.FEATURE_ACTION_BAR) || mActionBar != null) {
            return;
        }

        mActionBar = new WindowDecorActionBar(this);
        mActionBar.setDefaultDisplayHomeAsUpEnabled(mEnableDefaultActionBarUp);

        mWindow.setDefaultIcon(mActivityInfo.getIconResource());
        mWindow.setDefaultLogo(mActivityInfo.getLogoResource());
    }

在initWindowDecorActionBar()方法之后,activity会调用mActionBar.startActionMode(callback),那么刚好这里有实例化了mActionBar.

mActionBar = new WindowDecorActionBar(this);

所以说最终就是通过WindowDecorActionBar来完成相应的操作了。那么在去看看这个类里的方法:

    public ActionMode startActionMode(ActionMode.Callback callback) {
        if (mActionMode != null) {
            mActionMode.finish();
        }
        mOverlayLayout.setHideOnContentScrollEnabled(false);
        mContextView.killMode();
        ActionModeImpl mode = new ActionModeImpl(mContextView.getContext(), callback);
        if (mode.dispatchOnCreate()) {
            // This needs to be set before invalidate() so that it calls
            // onPrepareActionMode()
            mActionMode = mode;
            mode.invalidate();
            mContextView.initForMode(mode);
            animateToMode(true);
            if (mSplitView != null && mContextDisplayMode == CONTEXT_DISPLAY_SPLIT) {
                // TODO animate this
                if (mSplitView.getVisibility() != View.VISIBLE) {
                    mSplitView.setVisibility(View.VISIBLE);
                    if (mOverlayLayout != null) {
                        mOverlayLayout.requestApplyInsets();
                    }
                }
            }
            mContextView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
            return mode;
        }
        return null;
    }

这里有个方法 animateToMode(true);肯定是对布局进行一些操作,进去看看:


    void animateToMode(boolean toActionMode) {
        if (toActionMode) {
            showForActionMode();
        } else {
            hideForActionMode();
        }
        if (shouldAnimateContextView()) {
            Animator fadeIn, fadeOut;
            if (toActionMode) {
                fadeOut = mDecorToolbar.setupAnimatorToVisibility(View.GONE,
                        FADE_OUT_DURATION_MS);
                fadeIn = mContextView.setupAnimatorToVisibility(View.VISIBLE,
                        FADE_IN_DURATION_MS);
            } else {
                fadeIn = mDecorToolbar.setupAnimatorToVisibility(View.VISIBLE,
                        FADE_IN_DURATION_MS);
                fadeOut = mContextView.setupAnimatorToVisibility(View.GONE,
                        FADE_OUT_DURATION_MS);
            }
            AnimatorSet set = new AnimatorSet();
            set.playSequentially(fadeOut, fadeIn);
            set.start();
        } else {
            if (toActionMode) {
                mDecorToolbar.setVisibility(View.GONE);
                mContextView.setVisibility(View.VISIBLE);
            } else {
                mDecorToolbar.setVisibility(View.VISIBLE);
                mContextView.setVisibility(View.GONE);
            }
        }
        // mTabScrollView's visibility is not affected by action mode.
    }

这里有两个方法:showForActionMode(); hideForActionMode();一看就是用来隐藏或者显示布局的

    private void showForActionMode() {
        if (!mShowingForMode) {
            mShowingForMode = true;
            if (mOverlayLayout != null) {
                mOverlayLayout.setShowingForActionMode(true);
            }
            updateVisibility(false);
        }
    }
    private void hideForActionMode() {
        if (mShowingForMode) {
            mShowingForMode = false;
            if (mOverlayLayout != null) {
                mOverlayLayout.setShowingForActionMode(false);
            }
            updateVisibility(false);
        }
    }

他们最终会调用updateVisibility()这个方法:

    private void updateVisibility(boolean fromSystem) {
        // Based on the current state, should we be hidden or shown?
        final boolean shown = checkShowingFlags(mHiddenByApp, mHiddenBySystem,
                mShowingForMode);
        if (shown) {
            if (!mNowShowing) {
                mNowShowing = true;
                doShow(fromSystem);
            }
        } else {
            if (mNowShowing) {
                mNowShowing = false;
                doHide(fromSystem);
            }
        }
    }

updateVisibility()紧接着会调用doShow(fromSystem);doHide(fromSystem);

public void doShow(boolean fromSystem) {
        if (mCurrentShowAnim != null) {
            mCurrentShowAnim.end();
        }
        mContainerView.setVisibility(View.VISIBLE);
        if (mCurWindowVisibility == View.VISIBLE && (mShowHideAnimationEnabled
                || fromSystem)) {
            mContainerView.setTranslationY(0); // because we're about to ask its window loc
            float startingY = -mContainerView.getHeight();
            if (fromSystem) {
                int topLeft[] = {0, 0};
                mContainerView.getLocationInWindow(topLeft);
                startingY -= topLeft[1];
            }
            mContainerView.setTranslationY(startingY);
            AnimatorSet anim = new AnimatorSet();
            ObjectAnimator a = ObjectAnimator.ofFloat(mContainerView, View.TRANSLATION_Y, 0);
            a.addUpdateListener(mUpdateListener);
            AnimatorSet.Builder b = anim.play(a);
            if (mContentAnimations && mContentView != null) {
                b.with(ObjectAnimator.ofFloat(mContentView, View.TRANSLATION_Y,
                        startingY, 0));
            }
            if (mSplitView != null && mContextDisplayMode == CONTEXT_DISPLAY_SPLIT) {
                mSplitView.setTranslationY(mSplitView.getHeight());
                mSplitView.setVisibility(View.VISIBLE);
                b.with(ObjectAnimator.ofFloat(mSplitView, View.TRANSLATION_Y, 0));
            }
            anim.setInterpolator(AnimationUtils.loadInterpolator(mContext,
                    com.android.internal.R.interpolator.decelerate_cubic));
            anim.setDuration(250);
            // If this is being shown from the system, add a small delay.
            // This is because we will also be animating in the status bar,
            // and these two elements can't be done in lock-step.  So we give
            // a little time for the status bar to start its animation before
            // the action bar animates.  (This corresponds to the corresponding
            // case when hiding, where the status bar has a small delay before
            // starting.)
            anim.addListener(mShowListener);
            mCurrentShowAnim = anim;
            anim.start();
        } else {
            mContainerView.setAlpha(1);
            mContainerView.setTranslationY(0);
            if (mContentAnimations && mContentView != null) {
                mContentView.setTranslationY(0);
            }
            if (mSplitView != null && mContextDisplayMode == CONTEXT_DISPLAY_SPLIT) {
                mSplitView.setAlpha(1);
                mSplitView.setTranslationY(0);
                mSplitView.setVisibility(View.VISIBLE);
            }
            mShowListener.onAnimationEnd(null);
        }
        if (mOverlayLayout != null) {
            mOverlayLayout.requestApplyInsets();
        }
    }
    public void doHide(boolean fromSystem) {
        if (mCurrentShowAnim != null) {
            mCurrentShowAnim.end();
        }
        if (mCurWindowVisibility == View.VISIBLE && (mShowHideAnimationEnabled
                || fromSystem)) {
            mContainerView.setAlpha(1);
            mContainerView.setTransitioning(true);
            AnimatorSet anim = new AnimatorSet();
            float endingY = -mContainerView.getHeight();
            if (fromSystem) {
                int topLeft[] = {0, 0};
                mContainerView.getLocationInWindow(topLeft);
                endingY -= topLeft[1];
            }
            ObjectAnimator a = ObjectAnimator.ofFloat(mContainerView, View.TRANSLATION_Y, endingY);
            a.addUpdateListener(mUpdateListener);
            AnimatorSet.Builder b = anim.play(a);
            if (mContentAnimations && mContentView != null) {
                b.with(ObjectAnimator.ofFloat(mContentView, View.TRANSLATION_Y,
                        0, endingY));
            }
            if (mSplitView != null && mSplitView.getVisibility() == View.VISIBLE) {
                mSplitView.setAlpha(1);
                b.with(ObjectAnimator.ofFloat(mSplitView, View.TRANSLATION_Y,
                        mSplitView.getHeight()));
            }
            anim.setInterpolator(AnimationUtils.loadInterpolator(mContext,
                    com.android.internal.R.interpolator.accelerate_cubic));
            anim.setDuration(250);
            anim.addListener(mHideListener);
            mCurrentShowAnim = anim;
            anim.start();
        } else {
            mHideListener.onAnimationEnd(null);
        }
    }

这里面就是各种动画了。

另外总结一下

这里写图片描述
这是我画的一个view层次图,左边的是普通的Activity的,右边的是supportv7下的Activity也就是AppCompatActivity的。但是会发现,在Appcompatctivity中使用startActionMode,并不会将布局加载到ViewStub中去,而是会在ActionBarLayout下创建一个布局。
这个图的层次也不是绝对的,到底是什么样的根布局,都是由于activity根据我们自己设定的某些属性或者参数来决定的。比如说style里面的很多属性,我随便列举几个:

<item name="windowNoTitle">false</item>//这个是针对activity的
<item name="android:windowNoTitle">false</item>//这个是针对AppCompatActivity的

到底加载什么布局大家可以去网上搜搜android布局到底是如何加载出来的,或者是搜索setContentView到底干了啥。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
这个问题可能是由于 Android 平台默认使用了基于字符的文本选择器所导致的。在基于字符的文本选择器中,鼠标选择的区域会被限制在字符边界内。如果你想要在 Android 9.0 上实现在字符左右两侧都能选中文本的功能,可以尝试修改文本选择器的实现方式。 具体来说,你可以在 Android 源码中找到 `TextView` 类中的 `startSelectionActionMode()` 方法。这个方法会启动文本选择器,并传入一个 `SelectionActionModeCallback` 对象作为参数。你可以在这个回调对象中重写 `onUpdateSelection()` 方法,该方法会在文本选择器更新选中区域时被调用。你可以在这个方法中修改选中区域的范围,使其可以跨越字符边界。 下面是一个简单的示例代码: ```java public class MySelectionActionModeCallback implements ActionMode.Callback2 { private int mStart, mEnd; @Override public void onGetContentRect(ActionMode mode, View view, Rect outRect) { // 设置文本选择器的位置和大小 // ... } @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { // 创建文本选择器 // ... return true; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { // 准备文本选择器 // ... return true; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { // 处理文本选择器的菜单项点击事件 // ... return true; } @Override public void onDestroyActionMode(ActionMode mode) { // 销毁文本选择器 // ... } @Override public void onGetContentRect(ActionMode mode, View view, Rect outRect, Rect parentRect) { // 设置文本选择器的位置和大小 // ... } /** * 重写 onUpdateSelection() 方法,修改选中区域的范围 */ @Override public void onUpdateSelection(ActionMode mode, int start, int end, int textDirection, boolean textIsSelectable) { mStart = start; mEnd = end; CharSequence text = ((TextView) mode.getCustomView()).getText(); if (text != null && end > start) { // 获取选中区域内的字符串 String selectedText = text.subSequence(start, end).toString(); // 计算新的选中区域的范围 int newStart = start - selectedText.length() / 2; int newEnd = end + selectedText.length() / 2; // 设置新的选中区域 mode.setSelection(newStart, newEnd); mStart = newStart; mEnd = newEnd; } } } ``` 你可以在 `startSelectionActionMode()` 方法中传入这个回调对象,并在回调对象的 `onGetContentRect()` 方法中设置文本选择器的位置和大小。在 `onUpdateSelection()` 方法中,你可以获取当前选中区域的起始位置和结束位置,并计算出新的选中区域的范围。最后调用 `setSelection()` 方法设置新的选中区域即可。 需要注意的是,这个示例代码只是一个简单的示例,实际上需要考虑更多的情况,比如选中的文本跨越多行、多个段落等情况。你需要据实际情况对文本选择器的实现进行修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值