NavigationView下Menu点击切换Fragment的卡顿解决方法

项目中用到侧滑导航,点击导航切换Fragment时,菜单回弹会有很明显的卡顿。

首先想到的是Fragment的优化,是不是Fragment在OnCreateView的时候做了太多的工作。

方案1:将Fragment中的布局移出去,使用ViewStub预加载布局,在onCreateView时不对布局进行加载。在Fragment的onResume时使用viewStub.inflate()加载布局

运行发现,菜单回弹时的卡顿改善不是很明显,但有所改善。继续寻找解决方案。

后来想到,是不是fragment的事务管理有问题。

方案2:之前采用的是

transaction.hide(lastFragment).replace(R.id.home_content, insuranceFragment).commit();
replace这种替换方法是之前加载的被替换后就被移除,下次再加载时会重新生成。这样也会造成一定的资源浪费。

修改:

if (aboutFragment == null) {
                        aboutFragment = new AboutFragment();
                    }
                    transaction = fragmentManager.beginTransaction();
                    if (lastMenuItemId != menuItemId) {
                        //点击的不是当前选中菜单做如下处理,如点击的是当前选中菜单不做处理
                        if (aboutFragment.isAdded()) {
                            //当前Fragment已经被加载过
                            transaction.hide(lastFragment).show(aboutFragment).commit();
                        } else {
                            transaction.hide(lastFragment).add(R.id.home_content, aboutFragment).commit();
                        }
                        lastFragment = aboutFragment;
                        lastMenuItemId = menuItemId;
                    }

修改后运行发现,菜单回弹流畅度有了很大的提升,但是还是有轻微的卡顿。虽然这种卡顿是不注意不会发现的,高标准,严要求(小公司是不会给你十天半个月去优化一个卡顿的)。现在fragment已经找不到可以优化的点了,看DrawLayout吧。

方案3:对DrawLayout下手

@Override
    public boolean onNavigationItemSelected(MenuItem item) {

        menuItemId = item.getItemId();

        isSelectMenuItem = true;
       /**
         *之前的菜单点击判断,以及点击后的Fragment切换写在了这里*/
        if (drawer != null) {
            drawer.closeDrawer(GravityCompat.START);
        }

        return true;
    }
看这里,fragment的切换和drawer的close是同时进行的,close是有动画效果的,在刷新过程中同时执行了fragment的事务和初始化相关操作,这样UI线程处理的事情是不是太多?

先看一下close的动画时间是多长。左侧X轴滑动,找源码,在ViewDragHelper中发现:

private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
        final int startLeft = mCapturedView.getLeft();
        final int startTop = mCapturedView.getTop();
        final int dx = finalLeft - startLeft;
        final int dy = finalTop - startTop;

        if (dx == 0 && dy == 0) {
            // Nothing to do. Send callbacks, be done.
            mScroller.abortAnimation();
            setDragState(STATE_IDLE);
            return false;
        }

        final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
        mScroller.startScroll(startLeft, startTop, dx, dy, duration);

        setDragState(STATE_SETTLING);
        return true;
    }

final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);

private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) {
        xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity);
        yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity);
        final int absDx = Math.abs(dx);
        final int absDy = Math.abs(dy);
        final int absXVel = Math.abs(xvel);
        final int absYVel = Math.abs(yvel);
        final int addedVel = absXVel + absYVel;
        final int addedDistance = absDx + absDy;

        final float xweight = xvel != 0 ? (float) absXVel / addedVel :
                (float) absDx / addedDistance;
        final float yweight = yvel != 0 ? (float) absYVel / addedVel :
                (float) absDy / addedDistance;

        int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child));
        int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child));

        return (int) (xduration * xweight + yduration * yweight);
    }

int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child));

private int computeAxisDuration(int delta, int velocity, int motionRange) {
        if (delta == 0) {
            return 0;
        }

        final int width = mParentView.getWidth();
        final int halfWidth = width / 2;
        final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width);
        final float distance = halfWidth + halfWidth *
                distanceInfluenceForSnapDuration(distanceRatio);

        int duration;
        velocity = Math.abs(velocity);
        if (velocity > 0) {
            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
        } else {
            final float range = (float) Math.abs(delta) / motionRange;
            duration = (int) ((range + 1) * BASE_SETTLE_DURATION);
        }
        return Math.min(duration, MAX_SETTLE_DURATION);
    }

private static final int MAX_SETTLE_DURATION = 600; // ms

由源码可以看出,不管屏幕的宽度是多少,这个时间是在duration和MAX_SETTLE_DURATION之间取最小值的。在华为P8上断点测试,这个时间值是512ms。

卡顿的原因是页面在刷新时间间隔内做了太多工作,这个工作阻碍了屏幕的刷新操作,所以显得不流畅。那么可不可以在菜单回弹之后再去对Fragment的事务进行操作。

当时第一反应,既然最多600ms,那么就走600ms后进行操作。使用handler.post进行控制。后来想想,这样做是不是有点开销太大。继续阅读源码。。。

/**
     * Resolve the shared state of all drawers from the component ViewDragHelpers.
     * Should be called whenever a ViewDragHelper's state changes.
     */
    void updateDrawerState(int forGravity, @State int activeState, View activeDrawer) {
        final int leftState = mLeftDragger.getViewDragState();
        final int rightState = mRightDragger.getViewDragState();

        final int state;
        if (leftState == STATE_DRAGGING || rightState == STATE_DRAGGING) {
            state = STATE_DRAGGING;
        } else if (leftState == STATE_SETTLING || rightState == STATE_SETTLING) {
            state = STATE_SETTLING;
        } else {
            state = STATE_IDLE;
        }

        if (activeDrawer != null && activeState == STATE_IDLE) {
            final LayoutParams lp = (LayoutParams) activeDrawer.getLayoutParams();
            if (lp.onScreen == 0) {
                dispatchOnDrawerClosed(activeDrawer);
            } else if (lp.onScreen == 1) {
                dispatchOnDrawerOpened(activeDrawer);
            }
        }

        if (state != mDrawerState) {
            mDrawerState = state;

            if (mListeners != null) {
                // Notify the listeners. Do that from the end of the list so that if a listener
                // removes itself as the result of being called, it won't mess up with our iteration
                int listenerCount = mListeners.size();
                for (int i = listenerCount - 1; i >= 0; i--) {
                    mListeners.get(i).onDrawerStateChanged(state);
                }
            }
        }
    }


void dispatchOnDrawerClosed(View drawerView) {
        final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
        if ((lp.openState & LayoutParams.FLAG_IS_OPENED) == 1) {
            lp.openState = 0;

            if (mListeners != null) {
                // Notify the listeners. Do that from the end of the list so that if a listener
                // removes itself as the result of being called, it won't mess up with our iteration
                int listenerCount = mListeners.size();
                for (int i = listenerCount - 1; i >= 0; i--) {
                    mListeners.get(i).onDrawerClosed(drawerView);
                }
            }

            updateChildrenImportantForAccessibility(drawerView, false);

            // Only send WINDOW_STATE_CHANGE if the host has window focus. This
            // may change if support for multiple foreground windows (e.g. IME)
            // improves.
            if (hasWindowFocus()) {
                final View rootView = getRootView();
                if (rootView != null) {
                    rootView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
                }
            }
        }
    }

void dispatchOnDrawerOpened(View drawerView) {
        final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
        if ((lp.openState & LayoutParams.FLAG_IS_OPENED) == 0) {
            lp.openState = LayoutParams.FLAG_IS_OPENED;
            if (mListeners != null) {
                // Notify the listeners. Do that from the end of the list so that if a listener
                // removes itself as the result of being called, it won't mess up with our iteration
                int listenerCount = mListeners.size();
                for (int i = listenerCount - 1; i >= 0; i--) {
                    mListeners.get(i).onDrawerOpened(drawerView);
                }
            }

            updateChildrenImportantForAccessibility(drawerView, true);

            // Only send WINDOW_STATE_CHANGE if the host has window focus.
            if (hasWindowFocus()) {
                sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
            }

            drawerView.requestFocus();
        }
    }


在DrawLayout的状态发生改变时,mListeners回回调相关方法。对DrawLayout设置监听,在onDrawClosed中对Fragment的事务进行操作,这样避免了菜单滑动动画刷新和fragment事务的同时进行。

/**
     * Listener for monitoring events about drawers.
     */
    public interface DrawerListener {
        /**
         * Called when a drawer's position changes.
         * @param drawerView The child view that was moved
         * @param slideOffset The new offset of this drawer within its range, from 0-1
         */
        public void onDrawerSlide(View drawerView, float slideOffset);

        /**
         * Called when a drawer has settled in a completely open state.
         * The drawer is interactive at this point.
         *
         * @param drawerView Drawer view that is now open
         */
        public void onDrawerOpened(View drawerView);

        /**
         * Called when a drawer has settled in a completely closed state.
         *
         * @param drawerView Drawer view that is now closed
         */
        public void onDrawerClosed(View drawerView);

        /**
         * Called when the drawer motion state changes. The new state will
         * be one of {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
         *
         * @param newState The new drawer motion state
         */
        public void onDrawerStateChanged(@State int newState);
    }

运行结果,卡顿完全消失。。。代价:点击回弹后才能加载Fragment,时间半天。


你可能感兴趣的:(Android)