CoordinatorLayout問題彙總

如果你只打算學習一下CoordinatorLayout然後寫2個Demo試試,那麼本文並沒有什麼卵用,但是如果你打算在生產環境使用CoordinatorLayout,那麼強烈推薦閱讀一下本文,可以減少很多彎路,這個東西看起來很好,但是實際上坑也很多。

###前言
很多應用主頁常見的構造模式

一個包含ActionBar和Banner的header+ViewPager的組合模式
比如這樣:

然後需要滾動的時候能夠將 ActionBar和Banner滾出界面,但是又需要ViewPager的TabLayout能固定在屏幕頂部
比如滾動後是這樣:

Github上有很多sticky-viewpager xx-header-viewpager之類的項目提供類似的功能,但是大部分都會存在各種事件衝突問題,比如滑動不流暢,卡頓,彈跳之類的。

好在Android新的SupportLibrary提供了CoordinatorLayout能直接實現類似功能,具體如何使用參考 SupportLibrary中CoordinatorLayout的使用文檔即可,例子也可以直接看cheesesquare

我就談下使用過程中裏面的坑:

不支持ListView,不支持ScrollView,低版本不兼容ViewCompat.setNestedScrollingEnabled

PS.更新

support 23.1+新增了

ViewCompat.setNestedScrollingEnabled(listView/gridview,true); 

可以給ListView提供NestedScrolling支持,但是隻在LOLIPOP+版本上生效,下面的方案也一樣。

低版本只能使用RecyclerView


一些文檔/博客文章上說 可以使用一個 可滾動的組件
還有些錯誤的文章說 支持ListView

實際上該組件必須實現了 NestedScrollingChild接口

你可以使用RecyclerView,RecyclerView自身就實現了該接口,如果你要使用其他組件,那麼可能需要繼承原來的View並實現NestedScrollingChild接口

一個實現了NestedScrollingChild的Listview demo如下:

public class NestedScrollingListView extends ListView implements NestedScrollingChild {

    private final NestedScrollingChildHelper mScrollingChildHelper;

    public NestedScrollingListView(Context context) {
        super(context);
        mScrollingChildHelper = new NestedScrollingChildHelper(this);
        setNestedScrollingEnabled(true);
    }

    public NestedScrollingListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScrollingChildHelper = new NestedScrollingChildHelper(this);
        setNestedScrollingEnabled(true);
    }

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        mScrollingChildHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return mScrollingChildHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return mScrollingChildHelper.startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        mScrollingChildHelper.stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return mScrollingChildHelper.hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
                                        int dyUnconsumed, int[] offsetInWindow) {
        return mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
                dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }
}

其他View也可以使用類似的方式實現。

只對 5.0+ 生效,低版本無效。

RecyclerView滑動的時候不流暢

目前已知 r23.1 & r 23.2 都存在此問題,Google code的提交代碼裏倒是有相關修復記錄,但是目前還沒有發佈。

直接原因是Fling Direction錯誤,導致Fling事件被吃掉了,ACTION_UP/ACTION_CANCEL事件一旦發生,scroll動作直接停止

StackOverFlow上一個高票解決方案是重寫一個FlingBehavior,並配置給AppBar.

注意,這個Behavior是給AppBar的,不是給下面的可滾動組件的。

<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="your.package.FlingBehavior">
<!--your views here-->
 </android.support.design.widget.AppBarLayout>

public class FlingBehavior extends AppBarLayout.Behavior {

    private static final int TOP_CHILD_FLING_THRESHOLD = 3;
    private boolean isPositive;

    public FlingBehavior() {
    }

    public FlingBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
        if (velocityY > 0 && !isPositive || velocityY < 0 && isPositive) {
            velocityY = velocityY * -1;
        }
        if (target instanceof RecyclerView && velocityY < 0) {
            final RecyclerView recyclerView = (RecyclerView) target;
            final View firstChild = recyclerView.getChildAt(0);
            final int childAdapterPosition = recyclerView.getChildAdapterPosition(firstChild);
            consumed = childAdapterPosition > TOP_CHILD_FLING_THRESHOLD;
        }
        consumed=false;
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        isPositive = dy > 0;
    }

注意consumed計算這部分,會影響滾動形式

請參考父類滾動的實現代碼:

 @Override
public boolean onNestedFling(final CoordinatorLayout coordinatorLayout,
            final AppBarLayout child, View target, float velocityX, float velocityY,
            boolean consumed) {
        boolean flung = false;

        if (!consumed) {
            // It has been consumed so let's fling ourselves
            flung = fling(coordinatorLayout, child, -child.getTotalScrollRange(),
                    0, -velocityY);
        } else {
            // If we're scrolling up and the child also consumed the fling. We'll fake scroll
            // upto our 'collapsed' offset
            if (velocityY < 0) {
                // We're scrolling down
                final int targetScroll = -child.getTotalScrollRange()
                        + child.getDownNestedPreScrollRange();
                if (getTopBottomOffsetForScrollingSibling() < targetScroll) {
                    // If we're currently not expanded more than the target scroll, we'll
                    // animate a fling
                    animateOffsetTo(coordinatorLayout, child, targetScroll);
                    flung = true;
                }
            } else {
                // We're scrolling up
                final int targetScroll = -child.getUpNestedPreScrollRange();
                if (getTopBottomOffsetForScrollingSibling() > targetScroll) {
                    // If we're currently not expanded less than the target scroll, we'll
                    // animate a fling
                    animateOffsetTo(coordinatorLayout, child, targetScroll);
                    flung = true;
                }
            }
        }

        mWasNestedFlung = flung;
        return flung;
    }

確認你需要滾動哪一步分,你可以簡單根據RecyclerView展示的first item的position來判斷Recyclerview是否在top位置,也有另一種代碼如下:

@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
    if (target instanceof RecyclerView) {
        final RecyclerView recyclerView = (RecyclerView) target;
        consumed = velocityY > 0 || recyclerView.computeVerticalScrollOffset() > 0;
    }
    return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}

另一個解決方案是使用 smooth-app-bar-layout

具體如何使用請參考它自己的文檔

當header是Toolbar時,demo運作良好,當headerview高度較大時,滑動會發生劇烈抖動,彈跳,暫時還沒去看它源代碼,可能是我使用不正確

不支持adjustResize

可以參考這個問題
http://stackoverflow.com/questions/35599125/adjustresize-does-not-work-with-coordinatorlayout

解決方案是可以通過ViewTreeObserver.OnGlobalLayoutListener監聽界面改變,然後人工處理padding bottom

public class KeyboardUtil {
    private View decorView;
    private View contentView;

    public KeyboardUtil(Activity act, View contentView) {
        this.decorView = act.getWindow().getDecorView();
        this.contentView = contentView;

        //only required on newer android versions. it was working on API level 19
        if (Build.VERSION.SDK_INT >= 19) {
            decorView.getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener);
        }
    }

    public void enable() {
        if (Build.VERSION.SDK_INT >= 19) {
            decorView.getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener);
        }
    }

    public void disable() {
        if (Build.VERSION.SDK_INT >= 19) {
            decorView.getViewTreeObserver().removeOnGlobalLayoutListener(onGlobalLayoutListener);
        }
    }


    //a small helper to allow showing the editText focus
    ViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            Rect r = new Rect();
            //r will be populated with the coordinates of your view that area still visible.
            decorView.getWindowVisibleDisplayFrame(r);

            //get screen height and calculate the difference with the useable area from the r
            int height = decorView.getContext().getResources().getDisplayMetrics().heightPixels;
            int diff = height - r.bottom;

            //if it could be a keyboard add the padding to the view
            if (diff != 0) {
                // if the use-able screen height differs from the total screen height we assume that it shows a keyboard now
                //check if the padding is 0 (if yes set the padding for the keyboard)
                if (contentView.getPaddingBottom() != diff) {
                    //set the padding of the contentView for the keyboard
                    contentView.setPadding(0, 0, 0, diff);
                }
            } else {
                //check if the padding is != 0 (if yes reset the padding)
                if (contentView.getPaddingBottom() != 0) {
                    //reset the padding of the contentView
                    contentView.setPadding(0, 0, 0, 0);
                }
            }
        }
    };


    /**
     * Helper to hide the keyboard
     *
     * @param act
     */
    public static void hideKeyboard(Activity act) {
        if (act != null && act.getCurrentFocus() != null) {
            InputMethodManager inputMethodManager = (InputMethodManager) act.getSystemService(Activity.INPUT_METHOD_SERVICE);
            inputMethodManager.hideSoftInputFromWindow(act.getCurrentFocus().getWindowToken(), 0);
        }
    }
}

KeyboardUtil keyboardUtil = new KeyboardUtil(this, findViewById(android.R.id.content));

//enable it
keyboardUtil.enable();

ps.
fitSystemWindow 和 adjustResize 和 FLAG_TRANSLUCENT_STATUS 一起使用 一樣也會有衝突,造成界面縮放不正常。 當時也是用的這個解決方案。

CoordinatorLayout的onScrollListener只支持21+

解決方案:
在Behavior裏監聽滾動,並把數據傳出來

代碼和後一個問題貼在一起

注意 我這裏只監聽了 onDependentViewChanged
並在這裏調用onScroll接口,然後外部實際使用的數據是 headerview.getTop , 並不能覆蓋全部情況,但是我本身只用來做狀態欄漸變色,有其它使用場景請自行修改

當頂部容器不使用Toolbar時,Measure會有問題

解決方案:
自定義一個 Behavior,繼承AppBarLayout.ScrollingViewBehavior

並重載onMeasureChild方法

public class PatchedScrollingViewBehavior extends AppBarLayout.ScrollingViewBehavior {

    private OnScrollListener onScrollListener;

    public PatchedScrollingViewBehavior() {
        super();
    }

    public PatchedScrollingViewBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {

        Log.e("####", "onMeasureChild");


        if (child.getLayoutParams().height == -1) {
            List dependencies = parent.getDependencies(child);
            if (dependencies.isEmpty()) {
                return false;
            }

            AppBarLayout appBar = findFirstAppBarLayout(dependencies);
            if (appBar != null && ViewCompat.isLaidOut(appBar)) {
                if (ViewCompat.getFitsSystemWindows(appBar)) {
                    ViewCompat.setFitsSystemWindows(child, true);
                }

                int scrollRange = appBar.getTotalScrollRange();
//                int height = parent.getHeight() - appBar.getMeasuredHeight() + scrollRange;
                int parentHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
                int height = parentHeight - appBar.getMeasuredHeight() + scrollRange;
                int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST);
                parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);
                return true;
            }
        }

        return false;
    }

    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
    }

    private static AppBarLayout findFirstAppBarLayout(List<View> views) {
        int i = 0;
        for (int z = views.size(); i < z; ++i) {
            View view = (View) views.get(i);
            if (view instanceof AppBarLayout) {
                return (AppBarLayout) view;
            }
        }
        return null;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
                                          View dependency) {
        if (onScrollListener != null) {
            onScrollListener.onScroll(parent, child, dependency);
        }
        return super.onDependentViewChanged(parent, child, dependency);
    }

    public interface OnScrollListener {
        void onScroll(CoordinatorLayout parent, View child,
                      View dependency);

    }

    public OnScrollListener getOnScrollListener() {
        return onScrollListener;
    }

    public void setOnScrollListener(OnScrollListener onScrollListener) {
        this.onScrollListener = onScrollListener;
    }
}

REF:
http://stackoverflow.com/questions/30923889/flinging-with-recyclerview-appbarlayout
https://github.com/henrytao-me/smooth-app-bar-layout
https://code.google.com/p/android/issues/detail?id=177729

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章