J'ai créé un simple CollapsingToolbarLayout et cela fonctionne comme un charme. Mon problème est que si j'essaie d'utiliser un défilement lancé sur le nestedscrollview , il s'arrête juste lorsque je relâche mon doigt. Le défilement normal fonctionne comme il se doit.
Mon code d'activités est inchangé => auto généré une activité vide. (Je viens de cliquer sur créer une nouvelle activité vide dans Android studio et j'ai encore édité le XML).
J'ai lu ici, que les gestes de défilement sur l'image elle-même sont bogués, mais non, que le défilement est bogué: voir ici .
J'ai essayé d'activer "défilement fluide" via Java code. Il semble que si je défile suffisamment pour que l'image ne soit plus visible, les gestes de lancer sont alors reconnus.
TLDR: Pourquoi le geste de lancer ne fonctionne-t-il pas tant que l'image est visible? Mon code XML ressemble à ceci:
<Android.support.design.widget.CoordinatorLayout
xmlns:Android="http://schemas.Android.com/apk/res/Android"
xmlns:app="http://schemas.Android.com/apk/res-auto"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
Android:fitsSystemWindows="true">
<Android.support.design.widget.AppBarLayout
Android:id="@+id/profile_app_bar_layout"
Android:layout_width="match_parent"
Android:layout_height="wrap_content"
Android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
Android:fitsSystemWindows="true">
<Android.support.design.widget.CollapsingToolbarLayout
Android:id="@+id/profile_collapsing_toolbar_layout"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleMarginStart="48dp"
app:expandedTitleMarginEnd="64dp"
Android:fitsSystemWindows="true">
<ImageView
Android:id="@+id/image"
Android:layout_width="match_parent"
Android:layout_height="420dp"
Android:scaleType="centerCrop"
Android:fitsSystemWindows="true"
Android:src="@drawable/headerbg"
Android:maxHeight="192dp"
app:layout_collapseMode="parallax"/>
<Android.support.v7.widget.Toolbar
Android:id="@+id/toolbar"
Android:layout_width="match_parent"
Android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:layout_collapseMode="pin" />
</Android.support.design.widget.CollapsingToolbarLayout>
</Android.support.design.widget.AppBarLayout>
<Android.support.design.widget.FloatingActionButton
Android:id="@+id/fab"
app:layout_anchor="@id/profile_app_bar_layout"
app:layout_anchorGravity="bottom|right|end"
Android:layout_height="@dimen/fab_size_normal"
Android:layout_width="@dimen/fab_size_normal"
app:elevation="2dp"
app:pressedTranslationZ="12dp"
Android:layout_marginRight="8dp"
Android:layout_marginEnd="8dp"/>
<Android.support.v4.widget.NestedScrollView
Android:id="@+id/profile_content_scroll"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
Android:clipToPadding="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
Android:layout_gravity="fill_vertical"
Android:minHeight="192dp"
Android:overScrollMode="ifContentScrolls"
>
<RelativeLayout
Android:layout_width="match_parent"
Android:layout_height="wrap_content">
<TextView
Android:layout_width="match_parent"
Android:layout_height="wrap_content"
Android:text="@string/LoremIpsum"/>
</RelativeLayout>
</Android.support.v4.widget.NestedScrollView>
</Android.support.design.widget.CoordinatorLayout>
J'ai eu exactement le même problème avec CollapsingToolbarLayout avec ImageView à l'intérieur et NestedScrollView. Le défilement flottant s'arrête lorsque le doigt est relâché.
Cependant, j'ai remarqué quelque chose d'étrange. Si vous commencez à faire défiler avec votre doigt à partir d'une vue avec OnClickListener (par exemple Button), le défilement fling fonctionne parfaitement.
Je l'ai donc corrigé avec une solution bizarre. Définissez OnClickListener (qui ne fait rien) sur l'enfant direct de NestedScrollView. Ensuite, cela fonctionne parfaitement!
<Android.support.v4.widget.NestedScrollView
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"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
Android:id="@+id/content_container"
Android:layout_width="match_parent"
Android:layout_height="wrap_content"
Android:orientation="vertical">
<!-- Page Content -->
</LinearLayout>
</Android.support.v4.widget.NestedScrollView>
Donnez à l'enfant direct (LinearLayout) un identifiant et définissez OnClickListener dans Activity
ViewGroup mContentContainer = (ViewGroup) findViewById(R.id.content_container);
mContentContainer.setOnClickListener(this);
@Override
public void onClick(View view) {
int viewId = view.getId();
}
Notes:
Testé à l'aide de Support Design Library 25.0.1
CollapsingToolbarLayout avec scrollFlags = "scroll | enterAlwaysCollapsed"
Je sais que cette question a été posée il y a plus d'un an, mais ce problème ne semble toujours pas être résolu dans les bibliothèques de support/conception. Vous pouvez mettre en vedette problème afin qu'il se déplace plus haut dans la file d'attente prioritaire.
Cela dit, j'ai essayé la plupart des solutions publiées pour cela, y compris celle de patrick-iv sans succès. La seule façon que j'ai pu me mettre au travail était d'imiter le fling et de l'appeler par programme si un certain ensemble de conditions étaient détectées dans onPreNestedScroll()
. Au cours des quelques heures de mon débogage, j'ai remarqué que la fonction onNestedFling()
n'était jamais appelée vers le haut (défilement vers le bas) et semblait être consommée prématurément. Je ne peux pas dire avec 100% de certitude que cela fonctionnera pour 100% des implémentations, mais cela fonctionne assez bien pour mes utilisations, donc j'ai fini par me contenter de cela, même si c'est assez hacky et certainement pas ce que je voulais faire.
public class NestedScrollViewBehavior extends AppBarLayout.Behavior {
// Lower value means fling action is more easily triggered
static final int MIN_DY_DELTA = 4;
// Lower values mean less velocity, higher means higher velocity
static final int FLING_FACTOR = 20;
int mTotalDy;
int mPreviousDy;
WeakReference<AppBarLayout> mPreScrollChildRef;
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dx, int dy, int[] consumed) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
// Reset the total fling delta distance if the user starts scrolling back up
if(dy < 0) {
mTotalDy = 0;
}
// Only track move distance if the movement is positive (since the bug is only present
// in upward flings), equal to the consumed value and the move distance is greater
// than the minimum difference value
if(dy > 0 && consumed[1] == dy && MIN_DY_DELTA < Math.abs(mPreviousDy - dy)) {
mPreScrollChildRef = new WeakReference<>(child);
mTotalDy += dy * FLING_FACTOR;
}
mPreviousDy = dy;
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
View directTargetChild, View target, int nestedScrollAxes) {
// Stop any previous fling animations that may be running
onNestedFling(parent, child, target, 0, 0, false);
return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
}
@Override
public void onStopNestedScroll(CoordinatorLayout parent, AppBarLayout abl, View target) {
if(mTotalDy > 0 && mPreScrollChildRef != null && mPreScrollChildRef.get() != null) {
// Programmatically trigger fling if all conditions are met
onNestedFling(parent, mPreScrollChildRef.get(), target, 0, mTotalDy, false);
mTotalDy = 0;
mPreviousDy = 0;
mPreScrollChildRef = null;
}
super.onStopNestedScroll(parent, abl, target);
}
}
Et appliquez-le à l'AppBar
AppBarLayout scrollView = (AppBarLayout)findViewById(R.id.appbar);
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams)scrollView.getLayoutParams();
params.setBehavior(new NestedScrollViewBehavior());
J'ai essayé la solution de Floofer mais elle n'était toujours pas assez bonne pour moi. J'ai donc trouvé une meilleure version de son comportement. L'AppBarLayout se développe et s'effondre maintenant en douceur lors des lancers.
Remarque: J'ai utilisé la réflexion pour me frayer un chemin, donc cela peut ne pas fonctionner parfaitement avec une version de Android Bibliothèque de conception différente de 25.0.0.
public class SmoothScrollBehavior extends AppBarLayout.Behavior {
private static final String TAG = "SmoothScrollBehavior";
//The higher this value is, the faster the user must scroll for the AppBarLayout to collapse by itself
private static final int SCROLL_SENSIBILITY = 5;
//The real fling velocity calculation seems complex, in this case it is simplified with a multiplier
private static final int FLING_VELOCITY_MULTIPLIER = 60;
private boolean alreadyFlung = false;
private boolean request = false;
private boolean expand = false;
private int velocity = 0;
private int nestedScrollViewId;
public SmoothScrollBehavior(int nestedScrollViewId) {
this.nestedScrollViewId = nestedScrollViewId;
}
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
View target, int dx, int dy, int[] consumed) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
if(Math.abs(dy) >= SCROLL_SENSIBILITY) {
request = true;
expand = dy < 0;
velocity = dy * FLING_VELOCITY_MULTIPLIER;
} else {
request = false;
}
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
View directTargetChild, View target, int nestedScrollAxes) {
request = false;
return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
}
@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout appBarLayout, View target) {
if(request) {
NestedScrollView nestedScrollView = (NestedScrollView) coordinatorLayout.findViewById(nestedScrollViewId);
if (expand) {
//No need to force expand if it is already ready expanding
if (nestedScrollView.getScrollY() > 0) {
int finalY = getPredictedScrollY(nestedScrollView);
if (finalY <= 0) {
//since onNestedFling does not work to expand the AppBarLayout, we need to manually expand it
expandAppBarLayoutWithVelocity(coordinatorLayout, appBarLayout, velocity);
}
}
} else {
//onNestedFling will collapse the AppBarLayout with an animation time relative to the velocity
onNestedFling(coordinatorLayout, appBarLayout, target, 0, velocity, true);
if(!alreadyFlung) {
//TODO wait for AppBarLayout to be collapsed before scrolling for even smoother visual
nestedScrollView.fling(velocity);
}
}
}
alreadyFlung = false;
super.onStopNestedScroll(coordinatorLayout, appBarLayout, target);
}
private int getPredictedScrollY(NestedScrollView nestedScrollView) {
int finalY = 0;
try {
//With reflection, we can get the ScrollerCompat from the NestedScrollView to predict where the scroll will end
Field scrollerField = nestedScrollView.getClass().getDeclaredField("mScroller");
scrollerField.setAccessible(true);
Object object = scrollerField.get(nestedScrollView);
ScrollerCompat scrollerCompat = (ScrollerCompat) object;
finalY = scrollerCompat.getFinalY();
} catch (Exception e ) {
e.printStackTrace();
//If the reflection fails, it will return 0, which means the scroll has reached top
Log.e(TAG, "Failed to get mScroller field from NestedScrollView through reflection. Will assume that the scroll reached the top.");
}
return finalY;
}
private void expandAppBarLayoutWithVelocity(CoordinatorLayout coordinatorLayout, AppBarLayout appBarLayout, float velocity) {
try {
//With reflection, we can call the private method of Behavior that expands the AppBarLayout with specified velocity
Method animateOffsetTo = getClass().getSuperclass().getDeclaredMethod("animateOffsetTo", CoordinatorLayout.class, AppBarLayout.class, int.class, float.class);
animateOffsetTo.setAccessible(true);
animateOffsetTo.invoke(this, coordinatorLayout, appBarLayout, 0, velocity);
} catch (Exception e) {
e.printStackTrace();
//If the reflection fails, we fall back to the public method setExpanded that expands the AppBarLayout with a fixed velocity
Log.e(TAG, "Failed to get animateOffsetTo method from AppBarLayout.Behavior through reflection. Falling back to setExpanded.");
appBarLayout.setExpanded(true, true);
}
}
@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY) {
alreadyFlung = true;
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}
}
Pour l'utiliser, définissez un nouveau comportement sur votre AppBarLayout.
AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.app_bar);
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
params.setBehavior(new SmoothScrollBehavior(R.id.nested_scroll_view));
Je poste juste ceci ici afin que les autres ne le manquent pas dans les commentaires. La réponse de Jinang fonctionne à merveille, mais bravo à AntPachon pour avoir souligné une méthode beaucoup plus simple pour la même chose. Au lieu d'implémenter une méthode OnClick
sur le Child of the NestedScrollView
par programmation, une meilleure façon est de définir clickable=true
dans le xml pour l'enfant.
(En utilisant le même exemple que Jinang's )
<Android.support.v4.widget.NestedScrollView
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"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
Android:id="@+id/content_container"
Android:layout_width="match_parent"
Android:layout_height="wrap_content"
Android:orientation="vertical"
Android:clickable="true" > <!-- new -->
<!-- Page Content -->
</LinearLayout>
</Android.support.v4.widget.NestedScrollView>
Cette réponse a résolu ce problème pour moi. Créez un AppBarLayout.Behavior
Personnalisé comme celui-ci:
public final class FlingBehavior extends AppBarLayout.Behavior {
private static final int TOP_CHILD_FLING_THRESHOLD = 3;
private boolean isPositive;
public FlingBehavior() {
}
public FlingBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
if (velocityY > 0 && !isPositive || velocityY < 0 && isPositive) {
velocityY = velocityY * -1;
}
if (target instanceof RecyclerView && velocityY < 0) {
final RecyclerView recyclerView = (RecyclerView) target;
final View firstChild = recyclerView.getChildAt(0);
final int childAdapterPosition = recyclerView.getChildAdapterPosition(firstChild);
consumed = childAdapterPosition > TOP_CHILD_FLING_THRESHOLD;
}
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
isPositive = dy > 0;
}
}
et l'ajouter au AppBarLayout
comme ceci:
<Android.support.design.widget.AppBarLayout
Android:layout_width="match_parent"
Android:layout_height="wrap_content"
...
app:layout_behavior="com.example.test.FlingBehavior">