如何避免CollapsingToolbarLayout不被捕捉或滚动时“摇晃”?

背景

假设你已经创建了一个应用程序,它有一个类似于你可以通过“滚动活动”向导创建的UI,但是你希望滚动标志具有对齐性,例如:

 

问题

事实certificate,在许多情况下,它具有捕捉问题。 有时UI不会捕捉到顶部/底部,使CollapsingToolbarLayout停留在之间。

有时它也会尝试一个方向,然后决定打到另一个方向。

您可以在这里看到所附video的两个问题。

我试过了

我认为这是我在RecyclerView中使用setNestedScrollingEnabled(false)时得到的问题之一,所以我在这里问了一下 ,但后来我注意到,即使使用解决方案,根本不使用此命令,甚至在使用一个简单的NestedScrollView(由向导创建),我仍然可以注意到这种行为。

这就是为什么我决定在这里报告这个问题。

可悲的是,我无法findStackOverflow上那些奇怪的错误的解决方法。

这个问题

为什么会发生,更重要的是:我怎样才能避免这些问题,同时仍然使用它应该有的行为?


编辑:这是一个不错的改进Kotlin版本的接受答案:

 class RecyclerViewEx @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : RecyclerView(context, attrs, defStyle) { private var mAppBarTracking: AppBarTracking? = null private var mView: View? = null private var mTopPos: Int = 0 private var mLayoutManager: LinearLayoutManager? = null interface AppBarTracking { fun isAppBarIdle(): Boolean fun isAppBarExpanded(): Boolean } override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?, type: Int): Boolean { if (mAppBarTracking == null) return super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type) if (type == ViewCompat.TYPE_NON_TOUCH && mAppBarTracking!!.isAppBarIdle() && isNestedScrollingEnabled) { if (dy > 0) { if (mAppBarTracking!!.isAppBarExpanded()) { consumed!![1] = dy return true } } else { mTopPos = mLayoutManager!!.findFirstVisibleItemPosition() if (mTopPos == 0) { mView = mLayoutManager!!.findViewByPosition(mTopPos) if (-mView!!.top + dy <= 0) { consumed!![1] = dy - mView!!.top return true } } } } if (dy = 0 || mAppBarOffset <= -appBarLayout.totalScrollRange) } }) setAppBarTracking(object : AppBarTracking { override fun isAppBarIdle(): Boolean = appBarIdle.get() override fun isAppBarExpanded(): Boolean = appBarExpanded.get() }) } override fun fling(velocityX: Int, inputVelocityY: Int): Boolean { var velocityY = inputVelocityY if (mAppBarTracking != null && !mAppBarTracking!!.isAppBarIdle()) { val vc = ViewConfiguration.get(context) velocityY = if (velocityY < 0) -vc.scaledMinimumFlingVelocity else vc.scaledMinimumFlingVelocity } return super.fling(velocityX, velocityY) } } 

更新我已经稍微更改了代码来解决剩下的问题 – 至少是我可以重现的问题。 关键更新是仅在应用条被展开或折叠时处置dy 。 在第一次迭代中, dispatchNestedPreScroll()处理滚动而不检查AppBar状态是否处于折叠状态。

其他变化很小,属于清理范畴。 代码块在下面更新。


这个答案解决了有关RecyclerView的问题。 我所给出的其他答案仍然适用于此。 RecyclerView与支持库的26.0.0-beta2中引入的NestedScrollView具有相同的问题。

下面的代码是基于这个答案相关的问题,但包括修复了AppBar的不稳定的行为。 我已经删除了固定奇数滚动的代码,因为它不再需要。

AppBarTracking.java

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

MyRecyclerView.java

 public class MyRecyclerView extends RecyclerView { public MyRecyclerView(Context context) { this(context, null); } public MyRecyclerView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyRecyclerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } private AppBarTracking mAppBarTracking; private View mView; private int mTopPos; private LinearLayoutManager mLayoutManager; @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. // Only dy < 0 will open the AppBar. Stop it from opening by consuming dy if needed. mTopPos = mLayoutManager.findFirstVisibleItemPosition(); if (mTopPos == 0) { mView = mLayoutManager.findViewByPosition(mTopPos); if (-mView.getTop() + dy <= 0) { // Scroll until scroll position = 0 and AppBar is still collapsed. consumed[1] = dy - mView.getTop(); 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. if (offsetInWindow != null && !isNestedScrollingEnabled() && offsetInWindow[1] != 0) { offsetInWindow[1] = 0; } return returnValue; } @Override public void setLayoutManager(RecyclerView.LayoutManager layout) { super.setLayoutManager(layout); mLayoutManager = (LinearLayoutManager) getLayoutManager(); } public void setAppBarTracking(AppBarTracking appBarTracking) { mAppBarTracking = appBarTracking; } @SuppressWarnings("unused") private static final String TAG = "MyRecyclerView"; } 

ScrollingActivity.java

 public class ScrollingActivity extends AppCompatActivity implements AppBarTracking { private MyRecyclerView mNestedView; private int mAppBarOffset; private boolean mAppBarIdle = false; private int mAppBarMaxOffset; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_scrolling); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); mNestedView = findViewById(R.id.nestedView); final AppBarLayout appBar = findViewById(R.id.app_bar); 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); } }); appBar.post(new Runnable() { @Override public void run() { mAppBarMaxOffset = -appBar.getTotalScrollRange(); } }); findViewById(R.id.disableNestedScrollingButton).setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { // If the AppBar is fully expanded or fully collapsed (idle), then disable // expansion and apply the patch; otherwise, set a flag to disable the expansion // and apply the patch when the AppBar is idle. setExpandEnabled(false); } }); findViewById(R.id.enableNestedScrollingButton).setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { setExpandEnabled(true); } }); mNestedView.setAppBarTracking(this); mNestedView.setLayoutManager(new LinearLayoutManager(this)); mNestedView.setAdapter(new Adapter() { @Override public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) { return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate( android.R.layout.simple_list_item_1, parent, false)) { }; } @SuppressLint("SetTextI18n") @Override public void onBindViewHolder(final ViewHolder holder, final int position) { ((TextView) holder.itemView.findViewById(android.R.id.text1)).setText("item " + position); } @Override public int getItemCount() { return 100; } }); } private void setExpandEnabled(boolean enabled) { mNestedView.setNestedScrollingEnabled(enabled); } @Override public boolean isAppBarExpanded() { return mAppBarOffset == 0; } @Override public boolean isAppBarIdle() { return mAppBarIdle; } @SuppressWarnings("unused") private static final String TAG = "ScrollingActivity"; } 

这里发生了什么?

从这个问题来看,很显然,布局并没有像用户的手指不在屏幕上那样将应用程序栏关闭或打开。 拖动时,应用程序栏的行为应该如此。

在26.0.0-beta2版中,引入了一些新的方法 - 特别是用一个新的type参数调用dispatchNestedPreScroll() 。 type参数指定由dxdy指定的移动是由于用户触摸屏幕ViewCompat.TYPE_TOUCH还是不是ViewCompat.TYPE_NON_TOUCH

虽然导致问题的具体代码没有被识别,但修复的方法是在不需要垂直运动传播的情况下,通过dispatchNestedPreScroll() (需要处理dy )来杀死垂直运动。 实际上,应用程序栏在展开时将被锁定到位,并且将不允许开始关闭,直到通过触摸手势关闭为止。 应用程序栏在关闭时也会锁定,直到RecyclerView位于其最dy ,并且在执行触摸手势时,有足够的dy来打开应用程序栏。

所以,这不是一个解决问题条件的灰心。

MyRecyclerView代码的最后一部分处理在这个问题中被识别的问题,当嵌套滚动被禁用时处理不正确的滚动移动。 这是调用dispatchNestedPreScroll()更改offsetInWindow[1]的值之后的offsetInWindow[1] 。 这个代码背后的思想和接受的问题答案中提出的一样。 唯一的区别是,由于底层嵌套滚动代码已经改变,参数offsetInWindow有时是空的。 幸运的是,重要的是它似乎是非空的,所以最后一部分继续工作。

需要注意的是,这个“修复”对于所问的问题是非常具体的,而不是一个通用的解决方案。 这个解决方案可能会有一个非常短的保质期,因为我预计这个明显的问题将得到解决。

编辑代码已更新,使其更符合接受答案的代码。 这个答案涉及NestedScrollView而接受的答案是关于RecyclerView


这是API 26.0.0-beta2版本中引入的问题。 它不会在Beta 1版本或API 25上发生。如您所述,它也发生在API 26.0.0中。 一般来说,这个问题似乎与在beta2中如何处理循环和嵌套滚动有关。 有一个主要的重写嵌套滚动(见“继续滚动” ),所以这种types的问题已经出现并不奇怪。

我的想法是,多余的滚动不是正确处理NestedScrollView某处。 解决方法是在AppBar展开或折叠时静静地使用“非触摸”滚动的某些滚动( type == ViewCompat.TYPE_NON_TOUCH )。 这停止了​​弹跳,允许捕捉,并且通常使AppBar更好地表现。

ScrollingActivity已被修改以跟踪AppBar的状态,以报告它是否被展开。 一个新的类调用“MyNestedScrollView”会覆盖dispatchNestedPreScroll() (新的,见这里 )来操作多余滚动的消耗。

下面的代码应该足以阻止AppBarLayout摆动并拒绝捕捉。 (XML也将不得不改变,以适应MyNestedSrollView 。以下仅适用于支持库26.0.0-beta2及以上。)

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, "<<<