example - expandable toolbar android
¿Cómo evitar que CollapsingToolbarLayout no se ajuste o se “tambalee” cuando se desplaza? (3)
Fondo
Supongamos que tiene una aplicación que ha creado que tiene una IU similar a la que puede crear a través del asistente de "actividad de desplazamiento", pero desea que las banderas de desplazamiento aparezcan, como tal:
<android.support.design.widget.CollapsingToolbarLayout ... app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" >
El problema
Como resultado, en muchos casos tiene problemas de ajuste. A veces, la IU no se ajusta a la parte superior / inferior, lo que hace que CollapsingToolbarLayout permanezca en el medio.
A veces, también trata de ajustarse a una dirección y luego decide ajustarse a la otra.
Puedes ver ambos temas en el video adjunto here .
Lo que he intentado
Pensé que es uno de los problemas que obtuve cuando uso setNestedScrollingEnabled (false) en un RecyclerView interno, así que lo pregunté here , pero luego me di cuenta de que incluso con la solución y sin usar este comando en absoluto e incluso al usar Un simple NestedScrollView (creado por el asistente), todavía puedo notar este comportamiento.
Es por eso que decidí informar sobre esto como un problema, here .
Lamentablemente, no pude encontrar ninguna solución para esos errores extraños aquí en StackOverflow.
La pregunta
¿Por qué ocurre y, lo que es más importante, cómo puedo evitar esos problemas mientras sigo usando el comportamiento que se supone que tiene?
EDIT: aquí hay una buena versión mejorada de Kotlin de la respuesta aceptada:
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)
}
}
Parece que las llamadas onStartNestedScroll
y onStopNestedScroll
se pueden reordenar y llevan a un "wobbly" snap. Hice un pequeño truco dentro de AppBarLayout.Behavior. Realmente no quiero estropearme con todas esas cosas en la actividad como lo proponen otras respuestas.
@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);
}
}
Uso en diseño XML:
<android.support.design.widget.CoordinatorLayout>
<android.support.design.widget.AppBarLayout
app:layout_behavior="com.kaspersky.saas.ui.common.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>
Es muy probable que la causa raíz del problema en RecyclerView
. No tengo la oportunidad de profundizar más ahora.
Actualizar He cambiado el código ligeramente para solucionar los problemas restantes, al menos los que puedo reproducir. La actualización clave fue deshacerse de dy
solo cuando la barra de aplicaciones se expandió o colapsó. En la primera iteración, dispatchNestedPreScroll()
desechaba el desplazamiento sin verificar el estado de la barra de aplicaciones para un estado colapsado.
Otros cambios son menores y entran en la categoría de limpieza. Los bloques de código se actualizan a continuación.
Esta respuesta aborda el problema de la pregunta con respecto a RecyclerView
. La otra respuesta que he dado sigue en pie y se aplica aquí. RecyclerView
tiene los mismos problemas que NestedScrollView
que se introdujeron en 26.0.0-beta2 de las bibliotecas de soporte.
El código a continuación se basa en esta respuesta a una pregunta relacionada, pero incluye la solución para el comportamiento errático de la barra de aplicaciones. He eliminado el código que corrigió el desplazamiento extraño porque ya no parece ser necesario.
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";
}
¿Que está sucediendo aquí?
A partir de la pregunta, fue evidente que el diseño no cerraba o cerraba la barra de aplicaciones como debiera cuando el dedo del usuario no estaba en la pantalla. Al arrastrar, la barra de aplicaciones se comporta como debería.
En la versión 26.0.0-beta2, se introdujeron algunos métodos nuevos, específicamente dispatchNestedPreScroll() con un nuevo argumento de type
. El argumento de type
especifica si el movimiento especificado por dx
y dy
se debe a que el usuario toca la pantalla ViewCompat.TYPE_TOUCH
o no ViewCompat.TYPE_NON_TOUCH
.
Aunque el código específico que causa el problema no se identificó, la tachuela de la solución es eliminar el movimiento vertical en dispatchNestedPreScroll()
(deshacerse de dy
) cuando sea necesario al no permitir que el movimiento vertical se propague. En efecto, la barra de la aplicación se enclavará en su lugar cuando se expanda y no se permitirá que comience a cerrarse hasta que se cierre con un gesto táctil. La barra de la aplicación también se bloqueará cuando se cierre hasta que RecyclerView
se coloque en su posición más alta y haya suficiente dy
para abrir la barra de la aplicación mientras se realiza un gesto táctil.
Entonces, esto no es tanto una solución como un desaliento de las condiciones problemáticas.
La última parte del código de MyRecyclerView
trata un problema que se identificó en esta question relacionado con movimientos de desplazamiento incorrectos cuando el desplazamiento anidado está deshabilitado. Esta es la parte que viene después de la llamada al super de dispatchNestedPreScroll()
que cambia el valor de offsetInWindow[1]
. El pensamiento detrás de este código es el mismo que se presenta en la respuesta aceptada para la pregunta. La única diferencia es que, dado que el código de desplazamiento anidado subyacente ha cambiado, el argumento offsetInWindow
algún momento es nulo. Afortunadamente, parece que no es nulo cuando importa, por lo que la última parte sigue funcionando.
La advertencia es que este "arreglo" es muy específico para la pregunta y no es una solución general. Es probable que la solución tenga una vida útil muy corta, ya que espero que un problema tan obvio se resuelva en breve.
Editar El código se ha actualizado para que esté más en línea con el código de la respuesta aceptada. Esta respuesta se refiere a NestedScrollView
mientras que la respuesta aceptada es sobre RecyclerView
.
Este es un problema que se introdujo en la versión API 26.0.0-beta2. No sucede en la versión beta 1 o con API 25. Como notó, también ocurre con API 26.0.0. En general, el problema parece estar relacionado con la forma en que se manejan los lanzamientos y el desplazamiento anidado en beta2. Hubo una reescritura importante del desplazamiento anidado (consulte "Continuar con el desplazamiento" ), por lo que no es sorprendente que este tipo de problema haya surgido.
Mi pensamiento es que el exceso de desplazamiento no se está eliminando correctamente en algún lugar de NestedScrollView
. La type == ViewCompat.TYPE_NON_TOUCH
consiste en consumir de forma silenciosa ciertos desplazamientos que son desplazamientos "no táctiles" ( type == ViewCompat.TYPE_NON_TOUCH
) cuando la barra de aplicaciones se expande o se contrae. Esto detiene el rebote, permite instantáneas y, en general, hace que la barra de aplicaciones se comporte mejor.
ScrollingActivity
se ha modificado para rastrear el estado de la barra de aplicaciones para informar si está expandido o no. Una nueva llamada de clase "MyNestedScrollView" anula dispatchNestedPreScroll()
(la nueva, ver here ) para manipular el consumo del exceso de desplazamiento.
El siguiente código debería ser suficiente para evitar que AppBarLayout
y se niegue a AppBarLayout
. (XML también tendrá que cambiar para adaptarse a MyNestedSrollView
. Lo siguiente solo se aplica a la versión 26.0.0-beta2 y superior).
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, "<<<<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";
}