and*_*per 27 android android-collapsingtoolbarlayout
假设您创建的应用程序具有与您通过"滚动活动"向导创建的应用程序类似的UI,但您希望滚动标记具有捕捉功能,如下所示:
<android.support.design.widget.CollapsingToolbarLayout ... app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" >
Run Code Online (Sandbox Code Playgroud)
事实证明,在许多情况下,它存在捕捉问题.有时UI不会捕捉到顶部/底部,使得CollapsingToolbarLayout保持在两者之间.
有时它也会尝试捕捉到一个方向,然后决定捕捉到另一个方向.
您可以在此处查看所附视频的两个问题.
我认为这是我在RecyclerView上使用setNestedScrollingEnabled(false)时遇到的问题之一,所以我在这里问了一下,但后来我注意到即使使用解决方案也没有使用此命令,甚至在使用时一个简单的NestedScrollView(由向导创建),我仍然可以注意到这种行为.
这就是为什么我决定这个报告是一个问题,在这里.
可悲的是,我找不到StackOverflow上那些奇怪错误的解决方法.
为什么会发生,更重要的是:如何在仍然使用它应该具有的行为的同时避免这些问题?
编辑:这是一个很好的改进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 && type == ViewCompat.TYPE_TOUCH && mAppBarTracking!!.isAppBarExpanded()) {
consumed!![1] = dy
return true
}
val returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
if (offsetInWindow != null && !isNestedScrollingEnabled && offsetInWindow[1] != 0)
offsetInWindow[1] = 0
return returnValue
}
override fun setLayoutManager(layout: RecyclerView.LayoutManager) {
super.setLayoutManager(layout)
mLayoutManager = layoutManager as LinearLayoutManager
}
fun setAppBarTracking(appBarTracking: AppBarTracking) {
mAppBarTracking = appBarTracking
}
fun setAppBarTracking(appBarLayout: AppBarLayout) {
val appBarIdle = AtomicBoolean(true)
val appBarExpanded = AtomicBoolean()
appBarLayout.addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener {
private var mAppBarOffset = Integer.MIN_VALUE
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
if (mAppBarOffset == verticalOffset)
return
mAppBarOffset = verticalOffset
appBarExpanded.set(verticalOffset == 0)
appBarIdle.set(mAppBarOffset >= 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)
}
}
Run Code Online (Sandbox Code Playgroud)
更新
我稍微更改了代码以解决剩余问题 - 至少是我可以重现的问题.关键更新是dy仅在AppBar展开或折叠时进行处理.在第一次迭代中,dispatchNestedPreScroll()处理滚动而不检查折叠状态的AppBar的状态.
其他变化很小,属于清理类别.代码块在下面更新.
这个答案解决了有关问题的问题RecyclerView.我给出的另一个答案仍然适用于此处.与支持库的26.0.0-beta2中引入的RecyclerView问题相同NestedScrollView.
下面的代码基于对相关问题的这个答案,但包括对AppBar的不稳定行为的修复.我已经删除了修复奇数滚动的代码,因为它似乎不再需要了.
AppBarTracking.java
public interface AppBarTracking {
boolean isAppBarIdle();
boolean isAppBarExpanded();
}
Run Code Online (Sandbox Code Playgroud)
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";
}
Run Code Online (Sandbox Code Playgroud)
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";
}
Run Code Online (Sandbox Code Playgroud)
这里发生了什么?
从问题来看,很明显,当用户的手指不在屏幕上时,布局无法将应用栏关闭或打开.拖动时,应用栏的行为应该如此.
在版本26.0.0-beta2中,引入了一些新方法 - 特别是带有新参数的dispatchNestedPreScroll()type.所述type,如果所指定的运动参数指定dx和dy是由于用户触摸屏幕ViewCompat.TYPE_TOUCH 或不ViewCompat.TYPE_NON_TOUCH.
尽管没有确定导致问题的特定代码,但修复的方法是在不需要垂直移动传播的情况下在需要时消除dispatchNestedPreScroll()(处置dy)垂直移动.实际上,应用栏在展开时将被锁定到位,并且在通过触摸手势关闭之前不允许开始关闭.应用栏也将在关闭时锁定,直到RecyclerView位于其最顶部,并且dy在执行触摸手势时足以打开应用栏.
所以,这不是一个解决问题,而是对有问题的条件的劝阻.
MyRecyclerView代码的最后一部分处理了此问题中确定的问题,该问题涉及禁用嵌套滚动时的不正确滚动移动.这是调用super之后的部分,dispatchNestedPreScroll()它改变了值offsetInWindow[1].此代码背后的想法与问题的已接受答案中提供的内容相同.唯一的区别是,由于底层嵌套滚动代码已更改,因此参数offsetInWindow有时为null.幸运的是,它在重要时似乎是非空的,所以最后一部分继续工作.
需要注意的是,这个"修复"对于提出的问题非常具体,并不是一般解决方案.这个修复可能会有很短的保质期,因为我预计这个明显的问题很快就会得到解决.
看起来onStartNestedScroll和onStopNestedScroll调用可以重新排序,它导致"摇摇欲坠".我在AppBarLayout.Behavior中做了一个小黑客.不要真的想搞砸其他答案提出的活动中的所有东西.
@SuppressWarnings("unused")
public class ExtAppBarLayoutBehavior extends AppBarLayout.Behavior {
private int mStartedScrollType = -1;
private boolean mSkipNextStop;
public ExtAppBarLayoutBehavior() {
super();
}
public ExtAppBarLayoutBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {
if (mStartedScrollType != -1) {
onStopNestedScroll(parent, child, target, mStartedScrollType);
mSkipNextStop = true;
}
mStartedScrollType = type;
return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type);
}
@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target, int type) {
if (mSkipNextStop) {
mSkipNextStop = false;
return;
}
if (mStartedScrollType == -1) {
return;
}
mStartedScrollType = -1;
// Always pass TYPE_TOUCH, because want to snap even after fling
super.onStopNestedScroll(coordinatorLayout, abl, target, ViewCompat.TYPE_TOUCH);
}
}
Run Code Online (Sandbox Code Playgroud)
XML布局中的用法:
<android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.AppBarLayout
app:layout_behavior="com.example.ExtAppBarLayoutBehavior">
<!-- Put here everything you usually add to AppBarLayout: CollapsingToolbarLayout, etc... -->
</android.support.design.widget.AppBarLayout>
<!-- Content: recycler for example -->
<android.support.v7.widget.RecyclerView
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
...
</android.support.design.widget.CoordinatorLayout>
Run Code Online (Sandbox Code Playgroud)
它很可能是问题的根本原因RecyclerView.现在没有机会深入挖掘.
编辑代码已更新,使其更符合接受答案的代码.这个答案关注的NestedScrollView是接受的答案RecyclerView.
这是API 26.0.0-beta2版本中引入的问题.它不会发生在beta 1版本或API 25上.正如您所指出的,API 26.0.0也会发生这种情况.通常,问题似乎与beta2中如何处理flings和嵌套滚动有关.嵌套滚动有一个重大改写(参见"继续滚动"),因此出现这种类型的问题就不足为奇了.
我的想法是,多余的卷轴没有在某处妥善处理NestedScrollView.解决方法是type == ViewCompat.TYPE_NON_TOUCH在AppBar展开或折叠时静静地使用某些"非触摸"卷轴().这会停止弹跳,允许捕捉,通常会使AppBar表现更好.
ScrollingActivity已被修改以跟踪AppBar的状态以报告它是否被扩展.一个新的类调用"MyNestedScrollView"覆盖dispatchNestedPreScroll()(新的,请参见此处)来操纵多余滚动的消耗.
以下代码应足以阻止AppBarLayout摆动和拒绝捕捉.(XML也必须更改以适应MyNestedSrollView.以下仅适用于支持lib 26.0.0-beta2及更高版本.)
AppBarTracking.java
public interface AppBarTracking {
boolean isAppBarIdle();
boolean isAppBarExpanded();
}
Run Code Online (Sandbox Code Playgroud)
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";
}
Run Code Online (Sandbox Code Playgroud)
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, "<<<<offsetInWindow[1] forced to zero");
offsetInWindow[1] = 0;
}
return returnValue;
}
public void setAppBarTracking(AppBarTracking appBarTracking) {
mAppBarTracking = appBarTracking;
}
@SuppressWarnings("unused")
private static final String TAG = "MyNestedScrollView";
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
4396 次 |
| 最近记录: |