How to avoid CollapsingToolbarLayout not being snapped or being “wobbly” when scrolling?

前端 未结 4 1255
时光取名叫无心
时光取名叫无心 2020-12-07 11:37

Background

Suppose you have an app you\'ve created that has a similar UI as the one you can create via the wizard of \"scrolling activity\", yet you wish the scrol

4条回答
  •  情深已故
    2020-12-07 12:28

    Edit The code has been updated to bring it more in line with the code for the accepted answer. This answer concerns NestedScrollView while the accepted answer is about RecyclerView.


    This is an issue what was introduced in the API 26.0.0-beta2 release. It does not happen on the beta 1 release or with API 25. As you noted, it also happens with API 26.0.0. Generally, the problem seems to be related to how flings and nested scrolling are handled in beta2. There was a major rewrite of nested scrolling (see "Carry on Scrolling"), so it is not surprising that this type of issue has cropped up.

    My thinking is that excess scroll is not being disposed of properly somewhere in NestedScrollView. The work-around is to quietly consume certain scrolls that are "non-touch" scrolls (type == ViewCompat.TYPE_NON_TOUCH) when the AppBar is expanded or collapsed. This stops the bouncing, allows snaps and, generally, makes the AppBar better behaved.

    ScrollingActivity has been modified to track the status of the AppBar to report whether it is expanded or not. A new class call "MyNestedScrollView" overrides dispatchNestedPreScroll() (the new one, see here) to manipulate the consumption of the excess scroll.

    The following code should suffice to stop AppBarLayout from wobbling and refusing to snap. (XML will also have to change to accommodate MyNestedSrollView. The following only applies to support lib 26.0.0-beta2 and above.)

    AppBarTracking.java

    public interface AppBarTracking {
        boolean isAppBarIdle();
        boolean isAppBarExpanded();
    }
    

    ScrollingActivity.java

    public class ScrollingActivity extends AppCompatActivity implements AppBarTracking {
    
        private int mAppBarOffset;
        private int mAppBarMaxOffset;
        private MyNestedScrollView mNestedView;
        private boolean mAppBarIdle = true;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            AppBarLayout appBar;
    
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_scrolling);
            final Toolbar toolbar = findViewById(R.id.toolbar);
            setSupportActionBar(toolbar);
            appBar = findViewById(R.id.app_bar);
            mNestedView = findViewById(R.id.nestedScrollView);
            mNestedView.setAppBarTracking(this);
            appBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
                @Override
                public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                    mAppBarOffset = verticalOffset;
                }
            });
    
            appBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
                @Override
                public final void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                    mAppBarOffset = verticalOffset;
                    // mAppBarOffset = 0 if app bar is expanded; If app bar is collapsed then
                    // mAppBarOffset = mAppBarMaxOffset
                    // mAppBarMaxOffset is always <=0 (-AppBarLayout.getTotalScrollRange())
                    // mAppBarOffset should never be > zero or less than mAppBarMaxOffset
                    mAppBarIdle = (mAppBarOffset >= 0) || (mAppBarOffset <= mAppBarMaxOffset);
                }
            });
    
            mNestedView.post(new Runnable() {
                @Override
                public void run() {
                    mAppBarMaxOffset = mNestedView.getMaxScrollAmount();
                }
            });
        }
    
        @Override
        public boolean isAppBarIdle() {
            return mAppBarIdle;
        }
    
        @Override
        public boolean isAppBarExpanded() {
            return mAppBarOffset == 0;
        }
    
        @Override
        public boolean onCreateOptionsMenu(Menu menu) {
            // Inflate the menu; this adds items to the action bar if it is present.
            getMenuInflater().inflate(R.menu.menu_scrolling, menu);
            return true;
        }
    
        @Override
        public boolean onOptionsItemSelected(MenuItem item) {
            // Handle action bar item clicks here. The action bar will
            // automatically handle clicks on the Home/Up button, so long
            // as you specify a parent activity in AndroidManifest.xml.
            int id = item.getItemId();
    
            //noinspection SimplifiableIfStatement
            if (id == R.id.action_settings) {
                return true;
            }
            return super.onOptionsItemSelected(item);
        }
    
        @SuppressWarnings("unused")
        private static final String TAG = "ScrollingActivity";
    }
    

    MyNestedScrollView.java

    public class MyNestedScrollView extends NestedScrollView {
    
        public MyNestedScrollView(Context context) {
            this(context, null);
        }
    
        public MyNestedScrollView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public MyNestedScrollView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
    
            setOnScrollChangeListener(new View.OnScrollChangeListener() {
                @Override
                public void onScrollChange(View view, int x, int y, int oldx, int oldy) {
                    mScrollPosition = y;
                }
            });
        }
    
        private AppBarTracking mAppBarTracking;
        private int mScrollPosition;
    
        @Override
        public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
                                               int type) {
    
            // App bar latching trouble is only with this type of movement when app bar is expanded
            // or collapsed. In touch mode, everything is OK regardless of the open/closed status
            // of the app bar.
            if (type == ViewCompat.TYPE_NON_TOUCH && mAppBarTracking.isAppBarIdle()
                    && isNestedScrollingEnabled()) {
                // Make sure the AppBar stays expanded when it should.
                if (dy > 0) { // swiped up
                    if (mAppBarTracking.isAppBarExpanded()) {
                        // Appbar can only leave its expanded state under the power of touch...
                        consumed[1] = dy;
                        return true;
                    }
                } else { // swiped down (or no change)
                    // Make sure the AppBar stays collapsed when it should.
                    if (mScrollPosition + dy < 0) {
                        // Scroll until scroll position = 0 and AppBar is still collapsed.
                        consumed[1] = dy + mScrollPosition;
                        return true;
                    }
                }
            }
    
            boolean returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
            // Fix the scrolling problems when scrolling is disabled. This issue existed prior
            // to 26.0.0-beta2. (Not sure that this is a problem for 26.0.0-beta2 and later.)
            if (offsetInWindow != null && !isNestedScrollingEnabled() && offsetInWindow[1] != 0) {
                Log.d(TAG, "<<<

提交回复
热议问题