web-dev-qa-db-ja.com

NestedScrollViewおよびHorizo​​ntal RecyclerViewスムーズスクロール

水平なlayoutmanagerセットアップを備えた一連のrecyclerviewを含む単一の垂直のnestedscrollviewがあります。このアイデアは、新しいGoogle Playストアの外観とかなり似ています。機能させることはできますが、スムーズではありません。ここに問題があります:

1)水平方向のrecyclerviewアイテムは、右タップしても、ほとんどの場合、タッチイベントをインターセプトできません。ほとんどのモーションではスクロールビューが優先されるようです。横の動きに引っ掛かるのは難しいです。このUXは、機能するまでに数回試す必要があるため、イライラしています。 Playストアを確認すると、タッチイベントを非常にうまくインターセプトすることができ、うまく動作します。 Playストアで、1つの垂直リサイクラービュー内に多数の水平リサイクラービューが設定されていることに気付きました。スクロールビューなし。

2)水平方向のリサイクルビューの高さは手動で設定する必要があり、子要素の高さを簡単に計算する方法はありません。

これが私が使っているレイアウトです:

<Android.support.v4.widget.NestedScrollView
    Android:id="@+id/scroll"
    Android:layout_width="match_parent"
    Android:layout_height="match_parent"
    Android:clipToPadding="false"
    Android:background="@color/dark_bgd"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    <LinearLayout
        Android:layout_width="match_parent"
        Android:layout_height="wrap_content"
        Android:orientation="vertical">

        <LinearLayout
            Android:id="@+id/main_content_container"
            Android:layout_width="match_parent"
            Android:layout_height="wrap_content"
            Android:visibility="gone"
            tools:visibility="gone"
            Android:orientation="vertical">                

                <Android.support.v7.widget.RecyclerView
                    Android:id="@+id/starring_list"
                    Android:paddingLeft="@dimen/spacing_major"
                    Android:paddingRight="@dimen/spacing_major"
                    Android:layout_width="match_parent"
                    Android:layout_height="180dp" />

このUIパターンは非常に基本的であり、多くの異なるアプリで使用される可能性が高いです。私は多くのSOを読みましたが、リスト内にリストを置くのは悪い考えだとpplは言いますが、それはいたるところに使用されている非常に一般的で現代的なUIパターンです.netflixのような一連の水平スクロールリストを持つインターフェースを考えてください垂直リスト。これを実現するスムーズな方法はありませんか?

ストアからのサンプル画像:

Google Play Store

19
falc0nit3

スムーズスクロールの問題が修正されました。これは、設計サポートライブラリ(現在23.1.1)のNestedScrollViewのバグが原因でした。

あなたはここで問題と簡単な修正について読むことができます: https://code.google.com/p/Android/issues/detail?id=194398

つまり、フリングを実行した後、nestedscrollviewはスクローラーコンポーネントでコンプリートを登録しなかったため、後続のイベントのインターセプト(イートアップ)から親のnestedscrollviewを解放するために追加の 'ACTION_DOWN'イベントが必要でした。つまり、子リスト(またはビューページャー)をスクロールしようとすると、最初のタッチが親NSVバインドを解放し、その後のタッチが機能するようになります。それはUXを本当に悪くしていました。

基本的に、NSVのACTION_DOWNイベントに次の行を追加する必要があります。

computeScroll();

これが私が使っているものです:

public class MyNestedScrollView extends NestedScrollView {
private int slop;
private float mInitialMotionX;
private float mInitialMotionY;

public MyNestedScrollView(Context context) {
    super(context);
    init(context);
}

private void init(Context context) {
    ViewConfiguration config = ViewConfiguration.get(context);
    slop = config.getScaledEdgeSlop();
}

public MyNestedScrollView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
}

public MyNestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
}


private float xDistance, yDistance, lastX, lastY;

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    final float x = ev.getX();
    final float y = ev.getY();
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            xDistance = yDistance = 0f;
            lastX = ev.getX();
            lastY = ev.getY();

            // This is very important line that fixes 
           computeScroll();


            break;
        case MotionEvent.ACTION_MOVE:
            final float curX = ev.getX();
            final float curY = ev.getY();
            xDistance += Math.abs(curX - lastX);
            yDistance += Math.abs(curY - lastY);
            lastX = curX;
            lastY = curY;

            if (xDistance > yDistance) {
                return false;
            }
    }


    return super.onInterceptTouchEvent(ev);
}

}

このクラスをxmlファイルのネストされたscrollviewの代わりに使用すると、子リストがタッチイベントをインターセプトして適切に処理するはずです。

ふew、実際には、このようなバグがかなり多く、デザインサポートライブラリを完全に捨てて、成熟度が高まったら再訪したいと思っています。

20
falc0nit3

私はViewPagerで垂直スクロールの親で水平スクロールを実行することに成功しました:

<Android.support.v4.widget.NestedScrollView

    ...

    <Android.support.v4.view.ViewPager
        Android:id="@+id/pager_known_for"
        Android:layout_width="match_parent"
        Android:layout_height="350dp"
        Android:minHeight="350dp"
        Android:paddingLeft="24dp"
        Android:paddingRight="24dp"
        Android:clipToPadding="false"/>

パブリッククラスUniversityKnownForPagerAdapterはPagerAdapter {

public UniversityKnownForPagerAdapter(Context context) {
    mContext = context;
    mInflater = LayoutInflater.from(mContext);
}

@Override
public Object instantiateItem(ViewGroup container, int position) {
    View rootView = mInflater.inflate(R.layout.card_university_demographics, container, false);

    ...

    container.addView(rootView);

    return rootView;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    container.removeView((View)object);
}

@Override
public int getCount() {
    return 4;
}

@Override
public boolean isViewFromObject(View view, Object object) {
    return (view == object);
}

唯一の問題:ビューページャーに固定の高さを提供する必要がある

0

falc0nitソリューションが機能しなくなったため(現在、_28.0.0_バージョンのサポートライブラリを使用しているプロジェクト)、別のソリューションが見つかりました。

問題の背景の理由は同じで、スクロール可能なビューは2番目のタップでtrueを返すことでダウンイベントを食べますが、本来はそうすべきではありません。次のmoveイベントで反対側のスクロールを開始する問題はNestedScrollViewの場合と同様にRecyclerViewの場合と同様に再現されます。私の解決策は、ネイティブビューがonInterceptTouchEventでインターセプトできるようになる前に手動でスクロールを停止することです。この場合、_ACTION_DOWN_イベントはすでに停止されているため、これは食べられません。

したがって、NestedScrollViewの場合:

_class NestedScrollViewFixed(context: Context, attrs: AttributeSet) :
    NestedScrollView(context, attrs) {

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            onTouchEvent(ev)
        }
        return super.onInterceptTouchEvent(ev)
    }
}
_

RecyclerViewの場合:

_class RecyclerViewFixed(context: Context, attrs: AttributeSet) :
    RecyclerView(context, attrs) {

    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
        if (e.actionMasked == MotionEvent.ACTION_DOWN) {
            this.stopScroll()
        }
        return super.onInterceptTouchEvent(e)
    }

}
_

RecyclerViewのソリューションは読みやすいように見えますが、NestedScrollViewの場合は少し複雑です。残念ながら、ウィジェットで手動でスクロールを停止する明確な方法はありません。唯一の責任は、スクロール(omg)を管理することです。 abortAnimatedScroll()メソッドに興味がありますが、それはプライベートです。それを回避するためにリフレクションを使用することは可能ですが、私にとっては、abortAnimatedScroll()自体を呼び出すメソッドを呼び出すことをお勧めします。 _ACTION_DOWN_のonTouchEvent処理を見てください。

_ /*
 * If being flinged and user touches, stop the fling. isFinished
 * will be false if being flinged.
 */
if (!mScroller.isFinished()) {
    Log.i(TAG, "abort animated scroll");
    abortAnimatedScroll();
}
_

基本的にフリングの停止はこのメソッドで管理されますが、バグを修正するために呼び出すよりも少し遅れて

残念ながら、このため、OnTouchListenerを作成して外部に設定するだけでは不十分なので、要件を満たすのは継承のみです。

0
Beloo