web-dev-qa-db-ja.com

RecyclerViewにはスクロールするのに十分なコンテンツがありませんが、AppBarLayoutのツールバーはスクロール可能です

「appbar_scrolling_view_behavior」を持つメインコンテナには実際にスクロールするのに十分なコンテンツがありませんが、AppBarLayoutのツールバーはスクロール可能であることが本当に意図されていますか?

これまでにテストしたこと:
NestedScrollView( "wrap_content"属性付き)をメインコンテナーとして使用し、TextViewを子として使用すると、AppBarLayoutは適切に機能し、スクロールしません。

ただし、いくつかのエントリと「wrap_content」属性のみを使用してRecyclerViewを使用すると(スクロールする必要がないため)、RecyclerViewがスクロールイベントを受信しなくても(OnScrollChangeListenerでテスト済み)AppBarLayoutのツールバーがスクロール可能になります)。

これが私のレイアウトコードです。

<Android.support.design.widget.CoordinatorLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
    xmlns:app="http://schemas.Android.com/apk/res-auto"
    Android:id="@+id/coordinatorLayout"
    Android:layout_width="match_parent"
    Android:layout_height="match_parent">

    <Android.support.design.widget.AppBarLayout
        Android:id="@+id/appBarLayout"
        Android:layout_width="match_parent"
        Android:layout_height="wrap_content">

        <Android.support.v7.widget.Toolbar
            Android:id="@+id/toolbar"
            Android:layout_width="match_parent"
            Android:layout_height="?attr/actionBarSize"
            Android:background="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|enterAlways"
            app:theme="@style/ToolbarStyle" />
    </Android.support.design.widget.AppBarLayout>

    <Android.support.v7.widget.RecyclerView
        Android:id="@+id/recycler"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</Android.support.design.widget.CoordinatorLayout>

ツールバーは必要ではありませんが、スクロール可能という次の効果があります。

また、すべてのRecyclerViewアイテムが表示されているかどうかを確認し、RecyclerViewのsetNestedScrollingEnabled()メソッドを使用して、これに対処する方法を見つけました。
それにもかかわらず、それは私にとって意図したとおりのバグのように見えます。意見はありますか? :D

編集#1:

私の現在のソリューションに興味があるかもしれない人々のために、メソッドを呼び出すときに常に-1を返すLayoutManagerのために5ミリ秒の遅延でハンドラのpostDelayed()メソッドにsetNestedScrollingEnabled()ロジックを配置する必要がありました最初と最後のアイテムが表示されるかどうか。
onStart()メソッド(RecyclerViewの初期化後)およびRecyclerViewのコンテンツ変更が発生するたびにこのコードを使用します。

final LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
new Handler().postDelayed(new Runnable() {
    @Override
    public void run() {
        //no items in the RecyclerView
        if (mRecyclerView.getAdapter().getItemCount() == 0)
            mRecyclerView.setNestedScrollingEnabled(false);
        //if the first and the last item is visible
        else if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0
                && layoutManager.findLastCompletelyVisibleItemPosition() == mRecyclerView.getAdapter().getItemCount() - 1)
            mRecyclerView.setNestedScrollingEnabled(false);
        else
            mRecyclerView.setNestedScrollingEnabled(true);
    }
}, 5);

編集#2:

新しいアプリを試したところ、この(意図しない)動作はサポートライブラリバージョン23.3.0(またはそれ以前)で修正されたようです。したがって、回避策はもう必要ありません!

61
eickeee

したがって、適切な信用、この答えは私のためにほとんど解決しました https://stackoverflow.com/a/32923226/5050087 。しかし、実際にスクロール可能なリサイクラビューがあり、最後のアイテムが表示されたときにツールバーが表示されていなかったため(最初のスクロールアップではツールバーを表示しませんでした)、私はそれを変更し、より簡単な実装と動的に適応させることにしましたアダプター。

最初に、アプリバーのカスタムレイアウト動作を作成する必要があります。

public class ToolbarBehavior extends AppBarLayout.Behavior{

private boolean scrollableRecyclerView = false;
private int count;

public ToolbarBehavior() {
}

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

@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {
    return scrollableRecyclerView && super.onInterceptTouchEvent(parent, child, ev);
}

@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {
    updatedScrollable(directTargetChild);
    return scrollableRecyclerView && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type);
}

@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
    return scrollableRecyclerView && super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}

private void updatedScrollable(View directTargetChild) {
    if (directTargetChild instanceof RecyclerView) {
        RecyclerView recyclerView = (RecyclerView) directTargetChild;
        RecyclerView.Adapter adapter = recyclerView.getAdapter();
        if (adapter != null) {
            if (adapter.getItemCount()!= count) {
                scrollableRecyclerView = false;
                count = adapter.getItemCount();
                RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
                if (layoutManager != null) {
                    int lastVisibleItem = 0;
                    if (layoutManager instanceof LinearLayoutManager) {
                        LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
                        lastVisibleItem = Math.abs(linearLayoutManager.findLastCompletelyVisibleItemPosition());
                    } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                        StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
                        int[] lastItems = staggeredGridLayoutManager.findLastCompletelyVisibleItemPositions(new int[staggeredGridLayoutManager.getSpanCount()]);
                        lastVisibleItem = Math.abs(lastItems[lastItems.length - 1]);
                    }
                    scrollableRecyclerView = lastVisibleItem < count - 1;
                }
            }
        }
    } else scrollableRecyclerView = true;
  }
}

次に、レイアウトファイルでappbarに対してこの動作を定義するだけです。

<Android.support.design.widget.AppBarLayout
    Android:layout_width="match_parent"
    Android:layout_height="wrap_content"
    Android:fitsSystemWindows="true"
    app:layout_behavior="com.yourappname.whateverdir.ToolbarBehavior"
    >

画面の回転についてはテストしていないので、このように機能するかどうかを教えてください。回転が発生したときにcount変数が保存されるとは思わないので機能するはずですが、保存されない場合はお知らせください。

これは私にとって最も簡単でクリーンな実装でした。楽しんでください。

6
emirua

編集2:

RecyclerViewがスクロール可能でないときにツールバーがスクロール可能でないことを確認する唯一の方法は、RecyclerViewがスクロール可能であるかどうかを確認する必要があるsetScrollFlagsをプログラムで設定することです。このチェックは、アダプターが変更されるたびに実行する必要があります。

アクティビティと通信するためのインターフェース:

public interface LayoutController {
    void enableScroll();
    void disableScroll();
}

主な活動:

public class MainActivity extends AppCompatActivity implements 
    LayoutController {

    private CollapsingToolbarLayout collapsingToolbarLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        collapsingToolbarLayout = 
              (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);

        final FragmentManager manager = getSupportFragmentManager();
        final Fragment fragment = new CheeseListFragment();
        manager.beginTransaction()
                .replace(R.id.root_content, fragment)
                .commit();
    }

    @Override
    public void enableScroll() {
        final AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams)
                                  collapsingToolbarLayout.getLayoutParams();
        params.setScrollFlags(
                AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL 
                | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
        );
        collapsingToolbarLayout.setLayoutParams(params);
    }

    @Override
    public void disableScroll() {
        final AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams)
                                  collapsingToolbarLayout.getLayoutParams();
        params.setScrollFlags(0);
        collapsingToolbarLayout.setLayoutParams(params);
    }
}

activity_main.xml:

<Android.support.v4.widget.DrawerLayout
    xmlns:Android="http://schemas.Android.com/apk/res/Android"
    Android:id="@+id/drawer_layout"
    Android:layout_height="match_parent"
    Android:layout_width="match_parent"
    Android:fitsSystemWindows="true">

    <Android.support.design.widget.CoordinatorLayout
        xmlns:Android="http://schemas.Android.com/apk/res/Android"
        xmlns:app="http://schemas.Android.com/apk/res-auto"
        Android:id="@+id/main_content"
        Android:layout_width="match_parent"
        Android:layout_height="match_parent">

        <Android.support.design.widget.AppBarLayout
            Android:id="@+id/appbar"
            Android:layout_width="match_parent"
            Android:layout_height="wrap_content"
            Android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

            <Android.support.design.widget.CollapsingToolbarLayout
                Android:id="@+id/collapsing_toolbar"
                Android:layout_width="match_parent"
                Android:layout_height="match_parent"
                Android:fitsSystemWindows="true"
                app:contentScrim="?attr/colorPrimary">

                <Android.support.v7.widget.Toolbar
                    Android:id="@+id/toolbar"
                    Android:layout_width="match_parent"
                    Android:layout_height="?attr/actionBarSize"
                    Android:background="?attr/colorPrimary"
                    app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

            </Android.support.design.widget.CollapsingToolbarLayout>

        </Android.support.design.widget.AppBarLayout>

        <FrameLayout
            Android:id="@+id/root_content"
            Android:layout_width="match_parent"
            Android:layout_height="match_parent"
            Android:layout_gravity="fill_vertical"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

    </Android.support.design.widget.CoordinatorLayout>

</Android.support.v4.widget.DrawerLayout>

テストフラグメント:

public class CheeseListFragment extends Fragment {

    private static final int DOWN = 1;
    private static final int UP = 0;

    private LayoutController controller;
    private RecyclerView rv;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);

        try {
            controller = (MainActivity) getActivity();
        } catch (ClassCastException e) {
            throw new RuntimeException(getActivity().getLocalClassName()
                    + "must implement controller.", e);
        }
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        rv = (RecyclerView) inflater.inflate(
                R.layout.fragment_cheese_list, container, false);
        setupRecyclerView(rv);

        // Find out if RecyclerView are scrollable, delay required
        final Handler handler = new Handler();
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                if (rv.canScrollVertically(DOWN) || rv.canScrollVertically(UP)) {
                    controller.enableScroll();
                } else {
                    controller.disableScroll();
                }
            }
        }, 100);

        return rv;
    }

    private void setupRecyclerView(RecyclerView recyclerView) {
        final LinearLayoutManager layoutManager = new LinearLayoutManager(recyclerView.getContext());

        recyclerView.setLayoutManager(layoutManager);

        final SimpleStringRecyclerViewAdapter adapter =
                new SimpleStringRecyclerViewAdapter(
                        getActivity(),
                        // Test ToolBar scroll
                        getRandomList(/* with enough items to scroll */)
                        // Test ToolBar pin
                        getRandomList(/* with only 3 items*/)
                );

        recyclerView.setAdapter(adapter);
    }
}

ソース:

編集:

動作を制御するには、CollapsingToolbarLayoutを使用する必要があります。

ツールバーをAppBarLayoutに直接追加すると、enterAlwaysCollapsedおよびexitUntilCollapsedスクロールフラグにアクセスできますが、さまざまな要素が折りたたみにどのように反応するかを詳細に制御することはできません。 [...]セットアップはCollapsingToolbarLayoutのapp:layout_collapseMode = "pin"を使用して、ビューが折りたたまれている間、ツールバー自体が画面上部に固定されたままになるようにします。 http://Android-developers.blogspot.com。 tr/2015/05/Android-design-support-library.html

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

    <Android.support.v7.widget.Toolbar
        Android:id="@+id/drawer_toolbar"
        Android:layout_width="match_parent"
        Android:layout_height="?attr/actionBarSize"
        app:layout_collapseMode="pin"/>

</Android.support.design.widget.CollapsingToolbarLayout>

追加

app:layout_collapseMode="pin"

xmlのツールバーに。

    <Android.support.v7.widget.Toolbar
        Android:id="@+id/toolbar"
        Android:layout_width="match_parent"
        Android:layout_height="?attr/actionBarSize"
        Android:background="?attr/colorPrimary"
        app:layout_scrollFlags="scroll|enterAlways"
        app:layout_collapseMode="pin"
        app:theme="@style/ToolbarStyle" />
6
user3623735

AppBarLayoutにアタッチされる可能性のある独自のBehaviorクラスを使用して実装しました。

public class CustomAppBarLayoutBehavior extends AppBarLayout.Behavior {

private RecyclerView recyclerView;
private int additionalHeight;

public CustomAppBarLayoutBehavior(RecyclerView recyclerView, int additionalHeight) {
    this.recyclerView = recyclerView;
    this.additionalHeight = additionalHeight;
}

public boolean isRecyclerViewScrollable(RecyclerView recyclerView) {
    return recyclerView.computeHorizontalScrollRange() > recyclerView.getWidth() || recyclerView.computeVerticalScrollRange() > (recyclerView.getHeight() - additionalHeight);
}

@Override
public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes) {
    if (isRecyclerViewScrollable(mRecyclerView)) {
        return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
    }
    return false;
}

}

以下は、この動作を設定するコードです。

final View appBarLayout = ((DrawerActivity) getActivity()).getAppBarLayoutView();
CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
layoutParams.setBehavior(new AppBarLayoutNoEmptyScrollBehavior(recyclerView, getResources().getDimensionPixelSize(R.dimen.control_bar_height)));
1

これはバグではなく、viewGroup内のすべてのイベントはこの方法で処理されます。 recyclerviewはcoordinatorLayoutの子であるため、イベントが生成されるたびに、最初に親がチェックされ、親のみが関心を持たない場合は子に渡されます。グーグルを参照してください ドキュメント

1

LayoutManagerサブクラスのこのようなものは、望ましい動作をもたらすようです:

_@Override
public boolean canScrollVertically() {
    int firstCompletelyVisibleItemPosition = findFirstCompletelyVisibleItemPosition();
    if (firstCompletelyVisibleItemPosition == RecyclerView.NO_POSITION) return false;

    int lastCompletelyVisibleItemPosition = findLastCompletelyVisibleItemPosition();
    if (lastCompletelyVisibleItemPosition == RecyclerView.NO_POSITION) return false;

    if (firstCompletelyVisibleItemPosition == 0 &&
            lastCompletelyVisibleItemPosition == getItemCount() - 1)
        return false;

    return super.canScrollVertically();
}
_

canScrollVertically()のドキュメントには次のように書かれています:

_/**
 * Query if vertical scrolling is currently supported. The default implementation
 * returns false.
 *
 * @return True if this LayoutManager can scroll the current contents vertically
 */
_

現在の内容垂直にスクロールできます」という文言に注意してください。これは、現在の状態が戻り値に反映されるべきだと思います。

しかし、それはv7 recyclerviewライブラリ(23.1.1)を通じて提供されるLayoutManagerサブクラスのいずれによっても行われません。それが正しい解決策であるかどうかは少しheします。この質問で説明した状況以外の状況で望ましくない影響を引き起こす可能性があります。

1
joelpet

this 設計ライブラリ要素をサポートするためのサンプルを試すことをお勧めします。

これは、サンプルのレイアウトのようなレイアウトです。

<Android.support.design.widget.CoordinatorLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
    xmlns:app="http://schemas.Android.com/apk/res-auto"
    Android:id="@+id/main_content"
    Android:layout_width="match_parent"
    Android:layout_height="match_parent">

    <Android.support.design.widget.AppBarLayout
        Android:id="@+id/appbar"
        Android:layout_width="match_parent"
        Android:layout_height="wrap_content"
        Android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <Android.support.v7.widget.Toolbar
            Android:id="@+id/toolbar"
            Android:layout_width="match_parent"
            Android:layout_height="?attr/actionBarSize"
            Android:background="?attr/colorPrimary"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
            app:layout_scrollFlags="scroll|enterAlways" />

        <Android.support.design.widget.TabLayout
            Android:id="@+id/tabs"
            Android:layout_width="match_parent"
            Android:layout_height="wrap_content" />

    </Android.support.design.widget.AppBarLayout>

    <Android.support.v4.view.ViewPager
        Android:id="@+id/viewpager"
        Android:layout_width="match_parent"
        Android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</Android.support.design.widget.CoordinatorLayout>

おかげで、RecyclerViewのカスタムクラスを作成しましたが、キーはまだsetNestedScrollingEnabled()を使用しています。私の側ではうまくいきました。

public class RecyclerViewCustom extends RecyclerView implements ViewTreeObserver.OnGlobalLayoutListener
{
    public RecyclerViewCustom(Context context)
    {
        super(context);
    }

    public RecyclerViewCustom(Context context, @Nullable AttributeSet attrs)
    {
        super(context, attrs);
    }

    public RecyclerViewCustom(Context context, @Nullable AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
    }

    /**
     *  This supports scrolling when using RecyclerView with AppbarLayout
     *  Basically RecyclerView should not be scrollable when there's no data or the last item is visible
     *
     *  Call this method after Adapter#updateData() get called
     */
    public void addOnGlobalLayoutListener()
    {
        this.getViewTreeObserver().addOnGlobalLayoutListener(this);
    }

    @Override
    public void onGlobalLayout()
    {
        // If the last item is visible or there's no data, the RecyclerView should not be scrollable
        RecyclerView.LayoutManager layoutManager = getLayoutManager();
        final RecyclerView.Adapter adapter = getAdapter();
        if (adapter == null || adapter.getItemCount() <= 0 || layoutManager == null)
        {
            setNestedScrollingEnabled(false);
        }
        else
        {
            int lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition();
            boolean isLastItemVisible = lastVisibleItemPosition == adapter.getItemCount() - 1;
            setNestedScrollingEnabled(!isLastItemVisible);
        }

        unregisterGlobalLayoutListener();
    }

    private void unregisterGlobalLayoutListener()
    {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
        {
            getViewTreeObserver().removeOnGlobalLayoutListener(this);
        }
        else
        {
            getViewTreeObserver().removeGlobalOnLayoutListener(this);
        }
    }
}
0
Peter

ser3623735の回答に少し追加したいと思います。次のコードは絶対に間違っています。

// Find out if RecyclerView are scrollable, delay required
    final Handler handler = new Handler();
    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            if (rv.canScrollVertically(DOWN) || rv.canScrollVertically(UP)) {
                controller.enableScroll();
            } else {
                controller.disableScroll();
            }
        }
    }, 100);

そして、それが機能する場合でも-すべてのケースをカバーするわけではありません。データが100ミリ秒で表示されるという保証はまったくありません。また、onCreateViewメソッドだけでなく、データを使用するプロセスでデータがビューの高さを引き伸ばすことができます。そのため、次のコードを使用して、ビューの高さの変更を追跡する必要があります。

view.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
        @Override
        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
            if(bottom != oldBottom)
            {
                mActivity.setScrollEnabled(view.canScrollVertically(0) || view.canScrollVertically(1));
            }
        }
    });

さらに、スクロール状態を制御するために2つの別々のメソッドを作成する必要はありません。1つのsetScrollEnabledメソッドを使用する必要があります。

public void setScrollEnabled(boolean enabled) {
    final AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams)
            mToolbar.getLayoutParams();

    params.setScrollFlags(enabled ?
            AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS : 0);

    mToolbar.setLayoutParams(params);
}
0
Manunich