web-dev-qa-db-ja.com

スクロール時にCollapsingToolbarLayoutがスナップされない、または「ぐらつく」ことを避ける方法

バックグラウンド

「スクロールアクティビティ」のウィザードを使用して作成できるUIと同様のUIを作成したアプリがあるが、スクロールフラグにスナップさせたいとします。

<Android.support.design.widget.CollapsingToolbarLayout ... app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" >

問題

結局のところ、多くの場合、スナップの問題があります。 UIが上部/下部にスナップせず、CollapsingToolbarLayoutがその間に留まることがあります。

時々、一方の方向へのスナップを試みてから、もう一方の方向へのスナップを決定することもあります。

添付のビデオで両方の問題を確認できます here .

私が試したこと

それは、RecyclerViewでsetNestedScrollingEnabled(false)を使用するときに発生する問題の1つだと思ったので、それについて尋ねました here 、しかし、解決策があっても、このコマンドをまったく使用せず、単純なNestedScrollView(ウィザードによって作成される)を使用する場合でも、この動作に気付くことがあります。

それが問題として here として報告することにした理由です。

残念なことに、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)
    }
}
26

Update残りの問題に対処するためにコードを少し変更しました-少なくとも再現できる問題。重要な更新は、AppBarが展開または折りたたまれたときにのみdyを破棄することでした。最初の反復では、dispatchNestedPreScroll()は、折りたたみ状態のAppBarのステータスを確認せずにスクロールを破棄していました。

その他の変更はマイナーであり、クリーンアップのカテゴリに分類されます。コードブロックは以下で更新されます。


この回答は、RecyclerViewに関する質問の問題に対処します。私が与えた他の答えは、まだここに当てはまります。 RecyclerViewには、サポートライブラリの26.0.0-beta2で導入されたNestedScrollViewと同じ問題があります。

以下のコードは、関連する質問の この回答 に基づいていますが、AppBarの不安定な動作の修正が含まれています。奇妙なスクロールが不要になったので、これを修正するコードを削除しました。

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

ここで何が起こっていますか?

この質問から、ユーザーの指が画面上にないときに、レイアウトがアプリバーを閉じたり開いたりするのに失敗することが明らかでした。ドラッグすると、アプリバーは本来の動作をします。

バージョン26.0.0-beta2では、いくつかの新しいメソッドが導入されました-具体的には dispatchNestedPreScroll() 新しいtype引数付き。 type引数は、dxdyで指定された動きが、ユーザーが画面に触れたためかどうかを指定します _ViewCompat.TYPE_TOUCH_ - _ViewCompat.TYPE_NON_TOUCH_

問題を引き起こす特定のコードは特定されていませんが、修正のタックは、垂直移動を伝播させないことにより、必要に応じてdispatchNestedPreScroll()dyを破棄)の垂直移動を殺すことです。実際には、アプリバーは展開されたときに所定の位置にラッチされ、タッチジェスチャで閉じるまで閉じることはできません。アプリバーは、RecyclerViewが最上部に配置され、タッチジェスチャの実行中にアプリバーを開くのに十分なdyになるまで閉じたときにもラッチされます。

したがって、これは問題のある状態を落胆させるほどの修正ではありません。

MyRecyclerViewコードの最後の部分では、この question でネストされたスクロールが無効になっている場合の不適切なスクロールの動きで特定された問題を扱います。これは、_offsetInWindow[1]_の値を変更するdispatchNestedPreScroll()のスーパーへの呼び出しの後に来る部分です。このコードの背後にある考え方は、質問に対する受け入れられた回答で提示されたものと同じです。唯一の違いは、基になるネストされたスクロールコードが変更されているため、引数offsetInWindowがnullになる場合があることです。幸いなことに、重要な場合はnullでないように見えるため、最後の部分は引き続き機能します。

警告は、この「修正」は尋ねられた質問に非常に固有のものであり、一般的な解決策ではないということです。このような明らかな問題はまもなく対処されると予想されるため、修正の有効期間は非常に短い可能性があります。

9
Cheticamp

onStartNestedScrollonStopNestedScrollの呼び出しは並べ替えることができ、「ぐらつく」スナップにつながります。 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);
    }
}

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>

RecyclerViewに問題の根本原因がある可能性が非常に高いです。今より深く掘る機会はありません。

9
vyndor

Editコードは更新され、受け入れられた回答のコードにより一致するようになりました。この回答はNestedScrollViewに関するものですが、受け入れられる回答はRecyclerViewに関するものです。


これは、API 26.0.0-beta2リリースで導入された問題です。ベータ1リリースまたはAPI 25では発生しません。ご指摘のとおり、API 26.0.0でも発生します。一般的に、この問題は、ベータ2でのフリングとネストされたスクロールの処理方法に関連しているようです。ネストされたスクロールの大幅な書き直しが行われたため( "Carry on Scrolling" を参照)、この種の問題が発生したことは驚くことではありません。

私の考えでは、過剰なスクロールはNestedScrollViewのどこかに適切に処理されていません。回避策は、AppBarが展開または折り畳まれたときに、「非タッチ」スクロール(_type == ViewCompat.TYPE_NON_TOUCH_)である特定のスクロールを静かに消費することです。これにより、バウンドが停止し、スナップが可能になり、一般的に、AppBarの動作が改善されます。

ScrollingActivityは、AppBarが展開されているかどうかを報告するためにAppBarのステータスを追跡するように変更されました。新しいクラス呼び出し "MyNestedScrollView"は、dispatchNestedPreScroll()(新しいクラス、 here を参照)をオーバーライドして、余分なスクロールの消費を操作します。

次のコードで、AppBarLayoutが揺れたり、スナップを拒否したりするのを防ぐことができます。 (XMLはMyNestedSrollViewに対応するためにも変更する必要があります。以下はlib 26.0.0-beta2以降のサポートにのみ適用されます。)

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";
}
_
5
Cheticamp