项目中用到侧滑导航,点击导航切换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;
}
方案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
卡顿的原因是页面在刷新时间间隔内做了太多工作,这个工作阻碍了屏幕的刷新操作,所以显得不流畅。那么可不可以在菜单回弹之后再去对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();
}
}
/**
* 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);
}