Skip to content

Come evitare che CollapsingToolbarLayout non venga agganciato o sia "traballante" durante lo scorrimento?

Apprezziamo il vostro supporto per diffondere le nostre cronache in riferimento all'informatica.

Soluzione:

Aggiornare
Ho modificato leggermente il codice per risolvere i problemi rimanenti, almeno quelli che posso riprodurre. L'aggiornamento principale è stato quello di eliminare dy solo quando la barra delle applicazioni viene espansa o chiusa. Nella prima iterazione, dispatchNestedPreScroll() smaltiva lo scroll senza controllare lo stato dell'AppBar se era collassata.

Le altre modifiche sono minori e rientrano nella categoria della pulizia. I blocchi di codice sono aggiornati di seguito.


Questa risposta affronta il problema della domanda riguardante RecyclerView. L'altra risposta che ho dato è ancora valida e si applica anche in questo caso. RecyclerView ha gli stessi problemi di NestedScrollView che sono stati introdotti nella versione 26.0.0-beta2 delle librerie di supporto.

Il codice qui sotto si basa su questa risposta a una domanda correlata, ma include la correzione per il comportamento irregolare dell'AppBar. Ho rimosso il codice che correggeva lo strano scorrimento perché non sembra più necessario.

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";
}

Cosa succede qui?

Dalla domanda è emerso che il layout non riesce a far scattare la barra delle applicazioni chiusa o aperta come dovrebbe quando il dito dell'utente non è sullo schermo. Quando si trascina, la barra delle applicazioni si comporta come dovrebbe.

Nella versione 26.0.0-beta2 sono stati introdotti alcuni nuovi metodi, in particolare dispatchNestedPreScroll() con una nuova funzione type argomento. Il parametro type specifica se il movimento specificato da dx e dy sono dovuti al fatto che l'utente tocca lo schermo ViewCompat.TYPE_TOUCH o non ViewCompat.TYPE_NON_TOUCH.

Sebbene non sia stato identificato il codice specifico che causa il problema, la soluzione consiste nell'eliminare il movimento verticale in dispatchNestedPreScroll() (smaltire dy) quando necessario, evitando che il movimento verticale si propaghi. In effetti, la barra dell'applicazione viene bloccata in posizione quando viene espansa e non può iniziare a chiudersi finché non viene chiusa tramite un gesto tattile. La barra delle applicazioni sarà bloccata anche quando è chiusa, fino a quando il valore RecyclerView è posizionato all'estremo superiore e c'è una quantità sufficiente di dy per aprire la barra delle applicazioni durante l'esecuzione di un gesto tattile.

Quindi, non si tratta tanto di una correzione quanto di uno scoraggiamento di condizioni problematiche.

L'ultima parte del file MyRecyclerView si occupa di un problema identificato in questa domanda che riguarda i movimenti di scorrimento impropri quando lo scorrimento annidato è disabilitato. Questa è la parte che viene dopo la chiamata alla super di dispatchNestedPreScroll() che modifica il valore di offsetInWindow[1]. Il pensiero alla base di questo codice è lo stesso presentato nella risposta accettata alla domanda. L'unica differenza è che, poiché il codice di scorrimento annidato sottostante è cambiato, l'argomento offsetInWindow è talvolta nullo. Fortunatamente, sembra essere non nullo quando è importante, quindi l'ultima parte continua a funzionare.

L'avvertenza è che questa "correzione" è molto specifica per la domanda posta e non è una soluzione generale. Probabilmente la soluzione avrà una durata molto breve, poiché mi aspetto che un problema così ovvio venga risolto a breve.

Sembra che onStartNestedScroll e onStopNestedScroll possano essere riordinate e questo porti a uno scatto "traballante". Ho fatto un piccolo hack all'interno di AppBarLayout.Behavior. Non voglio davvero incasinare tutte queste cose nell'attività come proposto da altre risposte.

@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);
    }
}

Utilizzo nel layout XML:



    

        

    

    
    

    ...


È molto probabile che la causa principale del problema nel file RecyclerView. Non ho l'opportunità di scavare più a fondo ora.

Modifica Il codice è stato aggiornato per renderlo più conforme a quello della risposta accettata. Questa risposta riguarda NestedScrollView mentre la risposta accettata riguarda RecyclerView.


Si tratta di un problema introdotto nella versione API 26.0.0-beta2. Non si verifica con la release beta 1 o con l'API 25. Come avete notato, si verifica anche con l'API 26.0.0. In generale, il problema sembra essere legato al modo in cui i flings e lo scorrimento annidato sono gestiti nella beta2. C'è stata un'importante riscrittura dello scorrimento annidato (vedere "Scorrimento continuo"), quindi non è sorprendente che sia emerso questo tipo di problema.

Penso che lo scorrimento in eccesso non venga smaltito correttamente da qualche parte in NestedScrollView. La soluzione consiste nel consumare in modo silenzioso alcuni scorrimenti che sono scorrimenti "non touch" (type == ViewCompat.TYPE_NON_TOUCH) quando l'AppBar viene espansa o collassata. In questo modo si interrompe il rimbalzo, si consente lo snap e, in generale, si migliora il comportamento dell'AppBar.

ScrollingActivity è stato modificato per tracciare lo stato dell'AppBar e segnalare se è espansa o meno. Una nuova classe chiamata "MyNestedScrollView" sovrascrive dispatchNestedPreScroll() (quella nuova, vedere qui) per manipolare il consumo dello scroll in eccesso.

Il codice seguente dovrebbe essere sufficiente per fermare AppBarLayout di oscillare e rifiutarsi di scattare. (Anche l'XML dovrà essere modificato per accogliere MyNestedSrollView. Quanto segue si applica solo al supporto della lib 26.0.0-beta2 e successive).

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

Non dimenticare di comunicare questa dichiarazione se ti è stata utile.



Utilizzate il nostro motore di ricerca

Ricerca
Generic filters

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.