Nous avons une mise en page assez complexe qui contient CollapsingToolbarLayout, ainsi qu’une vue RecyclerView en bas.
Dans certains cas, nous désactivons temporairement le développement/la réduction de CollapsingToolbarLayout en appelant setNestedScrollingEnabled (boolean) sur RecyclerView.
Cela fonctionne généralement bien.
Cependant, dans certains cas (rares), le défilement lent sur le RecyclerView est semi-bloqué, ce qui signifie qu'il essaie de revenir en arrière lors du défilement. C'est comme si il y avait 2 défilement qui se battent (défilement vers le haut et vers le bas):
Le code à déclencher est en tant que tel:
res/layout/activity_scrolling.xml
<Android.support.design.widget.CoordinatorLayout
xmlns:Android="http://schemas.Android.com/apk/res/Android"
xmlns:app="http://schemas.Android.com/apk/res-auto"
xmlns:tools="http://schemas.Android.com/tools"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
Android:fitsSystemWindows="true"
tools:context="com.example.user.myapplication.ScrollingActivity">
<Android.support.design.widget.AppBarLayout
Android:id="@+id/app_bar"
Android:layout_width="match_parent"
Android:layout_height="@dimen/app_bar_height"
Android:fitsSystemWindows="true"
Android:theme="@style/AppTheme.AppBarOverlay">
<Android.support.design.widget.CollapsingToolbarLayout
Android:id="@+id/toolbar_layout"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
Android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<Android.support.v7.widget.Toolbar
Android:id="@+id/toolbar"
Android:layout_width="match_parent"
Android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/AppTheme.PopupOverlay"/>
</Android.support.design.widget.CollapsingToolbarLayout>
</Android.support.design.widget.AppBarLayout>
<Android.support.v7.widget.RecyclerView
Android:id="@+id/nestedView"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<LinearLayout
Android:layout_width="wrap_content"
Android:layout_height="wrap_content"
Android:orientation="horizontal"
app:layout_anchor="@id/app_bar"
app:layout_anchorGravity="bottom|end">
<Button
Android:id="@+id/disableNestedScrollingButton"
Android:layout_width="wrap_content"
Android:layout_height="wrap_content"
Android:text="disable"/>
<Button
Android:id="@+id/enableNestedScrollingButton"
Android:layout_width="wrap_content"
Android:layout_height="wrap_content"
Android:text="enable"
/>
</LinearLayout>
</Android.support.design.widget.CoordinatorLayout>
ScrollingActivity.Java
public class ScrollingActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scrolling);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
final RecyclerView nestedView = (RecyclerView) findViewById(R.id.nestedView);
findViewById(R.id.disableNestedScrollingButton).setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
nestedView.setNestedScrollingEnabled(false);
}
});
findViewById(R.id.enableNestedScrollingButton).setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
nestedView.setNestedScrollingEnabled(true);
}
});
nestedView.setLayoutManager(new LinearLayoutManager(this));
nestedView.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)) {
};
}
@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;
}
});
}
}
Au début, je pensais que c'était à cause de quelque chose d'autre (j'ai pensé que c'était une étrange combinaison avec DrawerLayout), mais j'ai ensuite trouvé un échantillon minimal à montrer, et c'est exactement ce que je pensais: tout cela est dû à setNestedScrollingEnabled.
J'ai essayé d'en informer le site Web de Google ( ici ), en espérant qu'il soit corrigé s'il s'agit d'un véritable bogue. Si vous souhaitez l'essayer ou regarder les vidéos du problème, allez-y, je ne peux pas tous les télécharger ici (trop volumineux et trop de fichiers).
J'ai aussi essayé d'utiliser des drapeaux spéciaux comme indiqué dans d'autres messages (exemples: ici , ici , ici , here et here ), mais aucun n'a aidé. En fait, chacun d’eux avait un problème, qu’il s’agisse de rester en mode développé ou de faire défiler d’une manière différente de la mienne.
Est-ce un problème connu? Pourquoi ça se passe?
Y a-t-il un moyen de surmonter cela?
Existe-t-il une alternative à l'appel de cette fonction de setNestedScrollingEnabled? Un sans aucun problème de défilement ou de verrouillage de l’état de CollapsingToolbarLayout?
Il s'agit d'une approche alternative pour atteindre le même objectif que cette réponse . Bien que cette réponse utilise Reflection, cette réponse ne le fait pas, mais le raisonnement reste le même.
Pourquoi cela arrive-t-il?
Le problème est que RecyclerView
utilise parfois une valeur périmée pour la variable membre mScrollOffset
. mScrollOffset
est défini à seulement deux endroits dans RecyclerView
: dispatchNestedPreScroll
et dispatchNestedScroll
. Nous ne sommes concernés que par dispatchNestedPreScroll
. Cette méthode est appelée par RecyclerView#onTouchEvent
quand elle gère les événements MotionEvent.ACTION_MOVE
.
Ce qui suit est extrait de la documentation de dispatchNestedPreScroll .
dispatchNestedPreScroll
boolean dispatchNestedPreScroll (int dx, int dy, int [] consommé, int [] offsetInWindow)
Distribue une étape d'un défilement imbriqué en cours avant que cette vue n'en consomme une partie.
Les événements de pré-défilement imbriqués sont des événements de défilement imbriqués, ce que l'interception tactile doit toucher. dispatchNestedPreScroll offre la possibilité à la vue parent dans une opération de défilement imbriquée de consommer tout ou partie de l'opération de défilement avant que la vue enfant ne l'utilise.
...
offsetInWindow int: facultatif. Si non null, retourne le décalage dans les coordonnées de la vue locale de cette vue d'avant cette opération à sa fin. Les implémentations de View peuvent l’utiliser pour ajuster le suivi des coordonnées d’entrée attendues.
offsetInWindow
est en fait un int[2]
avec le deuxième index représentant le décalage y à appliquer à la RecyclerView
en raison du défilement imbriqué.
RecyclerView#DispatchNestedPrescroll
est résolu en une méthode du même nom dans NestedScrollingChildHelper
.
Lorsque RecyclerView
appelle dispatchNestedPreScroll
, mScrollOffset
est utilisé comme argument offsetInWindow
. Ainsi, toute modification apportée à offsetInWindow
met directement à jour mScrollOffset
. dispatchNestedPreScroll
mises à jour mScrollOffset
tant que le défilement imbriqué est actif. Si le défilement imbriqué n’est pas activé, alors mScrollOffset
n’est pas mis à jour et procède à la dernière valeur définie par dispatchNestedPreScroll
. Ainsi, lorsque le défilement imbriqué est désactivé, la valeur de mScrollOffset
devient immédiatement obsolète, mais RecyclerView
continue de l’utiliser.
La valeur correcte de mScrollOffset[1]
au retour de dispatchNestedPreScroll
est le montant à ajuster pour input coordinate tracking
(voir ci-dessus). Dans RecyclerView
, les lignes suivantes ajustent la coordonnée y tactile:
mLastTouchY = y - mScrollOffset[1];
Si mScrollOffset[1]
est, disons, -30 (car il est périmé et devrait être nul), alors mLastTouchY
sera désactivé de +30 pixels (--30 = + 30). L’effet de cette erreur de calcul est qu’il apparaîtra que le toucher s’est produit plus loin en bas de l’écran. Ainsi, un défilement lent vers le bas fait défiler vers le haut et un défilement vers le haut défile plus rapidement. (Si un défilement vers le bas est assez rapide pour surmonter cette barrière 30px
, le défilement vers le bas aura lieu mais plus lentement qu'il ne le devrait.) Le défilement vers le haut sera trop rapide, car l'application pense que davantage d'espace a été couvert.
mScrollOffset
continuera en tant que variable périmée jusqu'à ce que le défilement imbriqué soit activé et dispatchNestedPreScroll
rapporte à nouveau la valeur correcte dans mScrollOffset
.
_ {Approche
Étant donné que mScrollOffset[1]
a une valeur périmée dans certaines circonstances, l'objectif est de lui attribuer la valeur correcte dans ces circonstances. Cette valeur doit être égale à zéro lorsque le défilement imbriqué n’a pas lieu, c’est-à-dire lorsque la barre d’application est développée ou réduite. Malheureusement, mScrollOffset
est local à RecyclerView
et il n'y a pas de passeur pour cela. Pour accéder à mScrollOffset
sans avoir recours à Reflection, une RecyclerView
personnalisée est créée qui remplace dispatchNestedPreScroll
. Le quatrième élément est offsetInWindow
, qui est la variable à modifier.
Une mScrollOffset
obsolète se produit chaque fois que le défilement imbriqué est désactivé pour la RecyclerView
. Une condition supplémentaire que nous imposerons est que la barre d’application doit être inactive afin que nous puissions dire en toute sécurité que mScrollOffset[1]
doit être égal à zéro. Ce n'est pas un problème car CollapsingToolbarLayout
spécifie snap
dans les drapeaux de défilement.
Dans l'exemple d'application, ScrollingActivity
a été modifié pour permettre l'enregistrement du développement et de la fermeture de la barre d'applications. Un rappel a également été créé (clampPrescrollOffsetListener
) qui retournera true
lorsque nos deux conditions sont remplies. Notre variable dispatchNestedPreScroll
invoquera ce rappel et pince mScrollOffset[1]
à zéro sur une réponse true
.
Le fichier source mis à jour pour ScrollingActivity
est présenté ci-dessous, de même que le RecyclerView - MyRecyclerView
personnalisé. Le fichier de présentation XML doit être modifié pour refléter la variable MyRecyclerView
personnalisée.
ScrollingActivity
public class ScrollingActivity extends AppCompatActivity
implements MyRecyclerView.OnClampPrescrollOffsetListener {
private CollapsingToolbarLayout mCollapsingToolbarLayout;
private AppBarLayout mAppBarLayout;
private MyRecyclerView mNestedView;
// This variable will be true when the app bar is completely open or completely collapsed.
private boolean mAppBarIdle = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scrolling);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mNestedView = (MyRecyclerView) findViewById(R.id.nestedView);
mAppBarLayout = (AppBarLayout) findViewById(R.id.app_bar);
mCollapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.toolbar_layout);
// Set the listener for the patch code.
mNestedView.setOnClampPrescrollOffsetListener(this);
// Listener to determine when the app bar is collapsed or fully open (idle).
mAppBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
@Override
public final void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
mAppBarIdle = verticalOffset == 0
|| verticalOffset <= appBarLayout.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.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)) {
};
}
@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);
}
// Return "true" when the app bar is idle and nested scrolling is disabled. This is a signal
// to the custom RecyclerView to clamp the y prescroll offset to zero.
@Override
public boolean clampPrescrollOffsetListener() {
return mAppBarIdle && !mNestedView.isNestedScrollingEnabled();
}
private static final String TAG = "ScrollingActivity";
}
MyRecyclerView
public class MyRecyclerView extends RecyclerView {
private OnClampPrescrollOffsetListener mPatchListener;
public MyRecyclerView(Context context) {
super(context);
}
public MyRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyRecyclerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
// Just a call to super plus code to force offsetInWindow[1] to zero if the patchlistener
// instructs it.
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
boolean returnValue;
int currentOffset;
returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
currentOffset = offsetInWindow[1];
Log.d(TAG, "<<<<dispatchNestedPreScroll: " + currentOffset);
if (mPatchListener.clampPrescrollOffsetListener() && offsetInWindow[1] != 0) {
Log.d(TAG, "<<<<dispatchNestedPreScroll: " + currentOffset + " -> 0");
offsetInWindow[1] = 0;
}
return returnValue;
}
public void setOnClampPrescrollOffsetListener(OnClampPrescrollOffsetListener patchListener) {
mPatchListener = patchListener;
}
public interface OnClampPrescrollOffsetListener {
boolean clampPrescrollOffsetListener();
}
private static final String TAG = "MyRecyclerView";
}
En fait, vous envisagez peut-être le problème de la mauvaise façon.
La seule chose dont vous avez besoin est de définir les indicateurs Toolbar
en conséquence. Vous ne faites pas vraiment autre chose alors je dirais que votre mise en page devrait être simplifiée comme suit:
<Android.support.design.widget.CoordinatorLayout
xmlns:Android="http://schemas.Android.com/apk/res/Android"
xmlns:app="http://schemas.Android.com/apk/res-auto"
xmlns:tools="http://schemas.Android.com/tools"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
Android:fitsSystemWindows="true"
tools:context="com.example.user.myapplication.ScrollingActivity">
<Android.support.design.widget.AppBarLayout
Android:id="@+id/app_bar"
Android:layout_width="match_parent"
Android:layout_height="@dimen/app_bar_height"
Android:fitsSystemWindows="true"
Android:theme="@style/AppTheme.AppBarOverlay">
<Android.support.v7.widget.Toolbar
Android:id="@+id/toolbar"
Android:layout_width="match_parent"
Android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:title="Title" />
</Android.support.design.widget.AppBarLayout>
<Android.support.v7.widget.RecyclerView
Android:id="@+id/nestedView"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<LinearLayout
Android:layout_width="wrap_content"
Android:layout_height="wrap_content"
Android:orientation="horizontal"
app:layout_anchor="@id/app_bar"
app:layout_anchorGravity="bottom|end">
<Button
Android:id="@+id/disableNestedScrollingButton"
Android:layout_width="wrap_content"
Android:layout_height="wrap_content"
Android:text="disable"/>
<Button
Android:id="@+id/enableNestedScrollingButton"
Android:layout_width="wrap_content"
Android:layout_height="wrap_content"
Android:text="enable"
/>
</LinearLayout>
</Android.support.design.widget.CoordinatorLayout>
Ensuite, lorsque vous souhaitez désactiver la réduction, définissez les indicateurs de votre barre d’outils:
// To disable collapsing
AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP);
toolbar.setLayoutParams(params);
Et pour permettre
// To enable collapsing
AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) toolbar.getLayoutParams();
params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL|AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS);
toolbar.setLayoutParams(params);
Conservez une référence aux paramètres de présentation si vous modifiez au lieu de l’obtenir tout le temps.
Si vous avez besoin deCollapsingToolbarLayout
get de et définir la LayoutParams
à cette View
à la place, mettez à jour les drapeaux de la même manière, mais ajoutez maintenant le appBarLayout.setExpanded(true/false)
Remarque: L'utilisation de la variable setScrollFlags
efface tous les indicateurs précédents. Soyez donc prudent et définissez tout requis indicateurs lorsque vous utilisez cette méthode.
dans la vue du recycleur, faire défiler en douceur
Android:nestedScrollingEnabled="false"
faire chevaucher la carte dans la barre d'outils
app:behavior_overlapTop = "24dp"
Essayez ce code pour CollapsingToolbar:
<Android.support.design.widget.CoordinatorLayout
Android:layout_width="match_parent"
Android:layout_height="match_parent"
Android:background="@color/background"
Android:fitsSystemWindows="true">
<Android.support.design.widget.AppBarLayout
Android:id="@+id/app_bar"
Android:layout_width="match_parent"
Android:layout_height="@dimen/app_bar_height"
Android:fitsSystemWindows="true"
Android:theme="@style/AppTheme.AppBarOverlay">
<Android.support.design.widget.CollapsingToolbarLayout
Android:id="@+id/toolbar_layout"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
Android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<Android.support.v7.widget.Toolbar
Android:id="@+id/toolbar"
Android:layout_width="match_parent"
Android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:title="Title" />
</Android.support.design.widget.CollapsingToolbarLayout>
</Android.support.design.widget.AppBarLayout>
<Android.support.v4.widget.NestedScrollView
Android:layout_width="match_parent"
Android:layout_height="match_parent"
Android:layout_marginLeft="10dp"
Android:layout_marginRight="10dp"
Android:background="@Android:color/transparent"
app:behavior_overlapTop="@dimen/behavior_overlap_top"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
Android:id="@+id/linearLayout"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
Android:orientation="vertical">
<Android.support.v7.widget.RecyclerView
Android:id="@+id/recycler_view
Android:layout_width="match_parent"
Android:layout_height="wrap_content"
Android:layout_margin="@dimen/text_min_padding"
Android:nestedScrollingEnabled="false"
Android:scrollbarSize="2dp"
Android:scrollbarStyle="outsideInset"
Android:scrollbarThumbVertical="@color/colorAccent"
Android:scrollbars="vertical" />
</LinearLayout>
</Android.support.v4.widget.NestedScrollView>
</Android.support.design.widget.CoordinatorLayout>
Comme @Moinkhan le fait remarquer, vous pouvez essayer d’emballer RecyclerView et les éléments suivants dans un NestedScrollView comme ceci, ceci devrait résoudre votre problème de défilement avec la présentation de votre barre d’outil qui se réduit:
<Android.support.design.widget.CoordinatorLayout
xmlns:Android="http://schemas.Android.com/apk/res/Android"
xmlns:app="http://schemas.Android.com/apk/res-auto"
xmlns:tools="http://schemas.Android.com/tools"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
Android:fitsSystemWindows="true"
tools:context="com.example.user.myapplication.ScrollingActivity">
<Android.support.design.widget.AppBarLayout
Android:id="@+id/app_bar"
Android:layout_width="match_parent"
Android:layout_height="@dimen/app_bar_height"
Android:fitsSystemWindows="true"
Android:theme="@style/AppTheme.AppBarOverlay">
<Android.support.design.widget.CollapsingToolbarLayout
Android:id="@+id/toolbar_layout"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
Android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<Android.support.v7.widget.Toolbar
Android:id="@+id/toolbar"
Android:layout_width="match_parent"
Android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/AppTheme.PopupOverlay"/>
</Android.support.design.widget.CollapsingToolbarLayout>
</Android.support.design.widget.AppBarLayout>
<Android.support.v4.widget.NestedScrollView
Android:layout_width="match_parent"
Android:layout_height="match_parent"
Android:layout_gravity="fill_vertical"
Android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<RelativeLayout
Android:layout_width="match_parent"
Android:layout_height="match_parent">
<Android.support.v7.widget.RecyclerView
Android:id="@+id/nestedView"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</RelativeLayout>
</Android.support.v4.widget.NestedScrollView>
<LinearLayout
Android:layout_width="wrap_content"
Android:layout_height="wrap_content"
Android:orientation="horizontal"
app:layout_anchor="@id/app_bar"
app:layout_anchorGravity="bottom|end">
<Button
Android:id="@+id/disableNestedScrollingButton"
Android:layout_width="wrap_content"
Android:layout_height="wrap_content"
Android:text="disable"/>
<Button
Android:id="@+id/enableNestedScrollingButton"
Android:layout_width="wrap_content"
Android:layout_height="wrap_content"
Android:text="enable"
/>
</LinearLayout>
</Android.support.design.widget.CoordinatorLayout>
Si le contenu de Recyclerview n’est pas affiché, vous pouvez suivre ce fil de discussion pour résoudre ce problème Comment utiliser RecyclerView dans NestedScrollView? .
J'espère que ça aide.
Je devais résoudre un problème similaire et le faire en utilisant un comportement personnalisé sur le AppBarLayout
. Tout fonctionne parfaitement . En remplaçant onStartNestedScroll
dans le comportement personnalisé, il est possible d'empêcher la réduction ou l'agrandissement de la présentation de la barre d'outils tout en conservant la vue de défilement (NestedScrollView
) dans mon cas et fonctionnant correctement. J'ai expliqué les détails ici , espérons que cela aide.
private class AppBarLayoutBehavior : AppBarLayout.Behavior() {
var canDrag = true
var acceptsNestedScroll = true
init {
setDragCallback(object : AppBarLayout.Behavior.DragCallback() {
override fun canDrag(appBarLayout: AppBarLayout): Boolean {
// Allow/Do not allow dragging down/up to expand/collapse the layout
return canDrag
}
})
}
override fun onStartNestedScroll(parent: CoordinatorLayout,
child: AppBarLayout,
directTargetChild: View,
target: View,
nestedScrollAxes: Int,
type: Int): Boolean {
// Refuse/Accept any nested scroll event
return acceptsNestedScroll
}}
Utilisez le code suivant, cela fonctionne très bien pour moi:
lockAppBarClosed();
ViewCompat.setNestedScrollingEnabled(recyclerView, false); // to lock the CollapsingToolbarLayout
et implémentez les méthodes suivantes:
private void setAppBarDragging(final boolean isEnabled) {
CoordinatorLayout.LayoutParams params =
(CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
AppBarLayout.Behavior behavior = new AppBarLayout.Behavior();
behavior.setDragCallback(new AppBarLayout.Behavior.DragCallback() {
@Override
public boolean canDrag(AppBarLayout appBarLayout) {
return isEnabled;
}
});
params.setBehavior(behavior);
}
public void unlockAppBarOpen() {
appBarLayout.setExpanded(true, false);
appBarLayout.setActivated(true);
setAppBarDragging(false);
}
public void lockAppBarClosed() {
appBarLayout.setExpanded(false, false);
appBarLayout.setActivated(false);
setAppBarDragging(false);
}
Je crois que ce problème est lié à la fermeture de la barre d’outils (fermée ou ouverte) et au fait de laisser une variable de décalage vertical (mScrollOffset[1]
dans RecyclerView
) avec une valeur non nulle qui polarise ensuite le défilement en ralentissant ou en inversant le défilement. direction et en accélérant dans l'autre. Cette variable ne semble être définie que dans NestedScrollingChildHelper
si le défilement imbriqué est activé. Ainsi, quelle que soit la valeur mScrollOffset[1]
reste inchangée une fois le défilement imbriqué désactivé.
Pour reproduire ce problème de manière fiable, vous pouvez enclencher la barre d’outils puis cliquer immédiatement sur désactiver. Voir cette vidéo pour une démonstration. Je crois que l’ampleur du problème varie en fonction du nombre de "claquements".
Si je fais glisser la barre d'outils en position complètement ouverte ou fermée et que je ne la laisse pas "claquer", je n'ai pas été en mesure de reproduire ce problème et mScrollOffset[1]
est défini sur zéro, ce qui, à mon avis, est la bonne valeur. J'ai également reproduit le problème en supprimant snap
du layout_scrollFlags
de la barre d'outils réduite dans la présentation et en plaçant la barre d'outils dans un état partiellement ouvert.
Si vous voulez jouer avec cela, vous pouvez mettre votre application de démonstration en mode débogage et observer la valeur de mScrollOffset[1]
dans RecyclerView#onTouchEvent
. Jetez également un coup d'œil aux méthodes NestedScrollingChildHelper
's dispatchNestedScroll
et dispatchNestedPreScroll
pour voir comment le décalage est défini uniquement lorsque le défilement imbriqué est activé.
Alors, comment résoudre ce problème? mScrollOffset
est privé àRecyclerView
et il n'est pas immédiatement évident de savoir comment sous-classer quoi que ce soit pour changer la valeur de mScrollOffset[1]
. Cela laisserait la réflexion, mais cela peut ne pas être souhaitable pour vous. Peut-être un autre lecteur a-t-il une idée sur la façon de l'aborder ou connaît-il une sauce secrète? Je republierai si quelque chose m'arrive.
Edit: J'ai fourni une nouvelle classe ScrollingActivity.Java
qui résout ce problème. Il utilise la réflexion et applique un correctif pour définir mScrollOffset[1]
of RecyclerView
à zéro lorsque le bouton de défilement désactivé a été enfoncé et que la barre d’application est inactive. J'ai fait des tests préliminaires et ça marche. Voici le Gist . (Voir Gist mis à jour ci-dessous.)
Deuxième édition: J'ai réussi à faire en sorte que la barre d'outils s'emboîte de manière amusante et reste bloquée au milieu sans le correctif. Il ne semble donc pas que le correctif soit à l'origine de ce problème particulier. Je peux faire en sorte que la barre d’outils rebondisse de complètement ouverte à réduite en faisant défiler assez rapidement vers le bas dans l’application non corrigée.
J'ai également examiné de nouveau le fonctionnement du correctif et je pense qu'il se comportera de manière autonome: la variable est privée et n'est référencée qu'à un seul endroit après la désactivation du défilement. Lorsque le défilement est activé, la variable est toujours réinitialisée avant utilisation. La vraie réponse est pour Google de résoudre ce problème. Jusqu'à ce qu'ils le fassent, je pense que c'est peut-être le plus proche possible d'une solution de contournement acceptable avec ce modèle particulier. (J'ai posté un Gist mis à jour qui traite des problèmes potentiels avec un clic rapide en laissant les commutateurs dans un état potentiellement inapproprié.)
Quoi qu'il en soit, le problème sous-jacent a été identifié et vous disposez d'un moyen fiable pour le reproduire. Vous pouvez ainsi plus facilement vérifier les autres solutions proposées.
J'espère que ça aide.
Je veux présenter une alternative de Nice, principalement basée sur celle ici :
AppBarLayoutEx.kt
class AppBarLayoutEx : AppBarLayout {
private var isAppBarExpanded = true
private val behavior = AppBarLayoutBehavior()
private var onStateChangedListener: (Boolean) -> Unit = {}
var enableExpandAndCollapseByDraggingToolbar: Boolean
get() = behavior.canDrag
set(value) {
behavior.canDrag = value
}
var enableExpandAndCollapseByDraggingContent: Boolean
get() = behavior.acceptsNestedScroll
set(value) {
behavior.acceptsNestedScroll = value
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
init {
addOnOffsetChangedListener(
AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
isAppBarExpanded = verticalOffset == 0
onStateChangedListener(isAppBarExpanded)
})
}
override fun setLayoutParams(params: ViewGroup.LayoutParams?) {
super.setLayoutParams(params)
(params as CoordinatorLayout.LayoutParams).behavior = behavior
}
fun toggleExpandedState() {
setExpanded(!isAppBarExpanded, true)
}
fun setOnExpandAndCollapseListener(onStateChangedListener: (Boolean) -> Unit) {
this.onStateChangedListener = onStateChangedListener
}
private class AppBarLayoutBehavior : AppBarLayout.Behavior() {
var canDrag = true
var acceptsNestedScroll = true
init {
setDragCallback(object : AppBarLayout.Behavior.DragCallback() {
override fun canDrag(appBarLayout: AppBarLayout) = canDrag
})
}
override fun onStartNestedScroll(parent: CoordinatorLayout, child: AppBarLayout, directTargetChild: View,
target: View, nestedScrollAxes: Int, type: Int) = acceptsNestedScroll
}
}
Utilisation: en plus de l’utiliser dans le fichier XML de présentation, vous pouvez désactiver/activer son développement en utilisant:
appBarLayout.enableExpandAndCollapseByDraggingToolbar = true/false
appBarLayout.enableExpandAndCollapseByDraggingContent = true/false