ViewPager2の中で、Fragmentのナビゲーションを実装する方法【七転び八起き】

androidx.viewpager2.widget.ViewPager2はすごいです。
こういう画面の下部のナビゲーションに応答して切りかえを行う、上の部分のコンテナです。

アダプターを用いていて、その実装として、切り替えた先それぞれがフラグメントとなっています。
NavHostFragmentを用いることで、階層構造も実現できますね。
(画像は、現在開発中の次期バージョンです)

レイアウトXMLから見ていきましょう。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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:id="@+id/drawerLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context=".MainActivity">

    <com.google.android.material.navigation.NavigationView
        android:id="@+id/navSideView"
        android:layout_width="100dp"
        android:layout_height="0dp"
        android:layout_gravity="start"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:menu="@menu/bottom_nav_menu" />
   <!--  app:layout_constraintTop_toBottomOf="@+id/mainActivityToolbar" -->

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/myViewPager"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:defaultNavHost="true"

        app:layout_constraintBottom_toTopOf="@id/navBottomView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/navSideView"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/navBottomView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:menu="@menu/bottom_nav_menu" />

</androidx.constraintlayout.widget.ConstraintLayout>

navSideView – 画面が横長の場合のみ表示するナビ
 クラス:com.google.android.material.navigation.NavigationView

navBottomView – 画面が縦長の場合のみ表示するナビ
 クラス:com.google.android.material.bottomnavigation.BottomNavigationView

myViewPager – ナビによって制御される部分
 クラス:androidx.viewpager2.widget.ViewPager2

縦長の場合画面下部に、横長の場合画面左にナビを配置する予定です。
そのため、AppBarLayoutをネストしています。

MainActivity.java
    MyPagerAdapter _pagerAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        _pagerAdapter = new MyPagerAdapter(this);
        binding.myViewPager.setUserInputEnabled(false); //アダプターがスワイプに反応しないようにする 
        binding.myViewPager.setAdapter(_pagerAdapter); 
    }

    @Override
    protected void onResume() {
        super.onResume();

        binding.navBottomView.setLabelVisibilityMode(BottomNavigationView.LABEL_VISIBILITY_LABELED);
        binding.navBottomView.setItemIconTintList(null);
        binding.navSideView.setItemIconTintList(null);
        binding.myViewPager.setUserInputEnabled(false);
        Menu menu = binding.navBottomView.getMenu();
        binding.navBottomView.setOnItemSelectedListener(item -> {
            for (int i = 0; i < menu.size(); ++ i) {
                if (menu.getItem(i).getItemId() == item.getItemId()) {
                    binding.myViewPager.setCurrentItem(i, true);
                    break;
                }
            }
            return true;
        });
        binding.navSideView.setNavigationItemSelectedListener(item -> {
            for (int i = 0; i < menu.size(); ++ i) {
                if (menu.getItem(i).getItemId() == item.getItemId()) {
                    binding.myViewPager.setCurrentItem(i, true);
                    break;
                }
            }
            return true;
        });
    }

メニューXMLの宣言はたとえばこんな感じでいかがでしょうか。

bottom_nav_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/navigation_pad_2"
        android:icon="@drawable/icon_pad_2"
        android:title="@string/title_control" />

    <item
        android:id="@+id/navigation_piano_2"
        android:icon="@drawable/icon_piano_2"
        android:title="@string/title_piano" />

    <item
        android:id="@+id/navigation_midi_2"
        android:icon="@drawable/icon_midi_2"
        android:title="@string/title_midi" />

    <item
        android:id="@+id/navigation_config_2"
        android:icon="@drawable/ic_config"
        android:title="@string/title_config" />

    <item
        android:id="@+id/navigation_sponsor_2"
        android:icon="@drawable/ic_launcher"
        android:title="@string/title_support" />

</menu>

BottomNavigationViewの、app:menu=”@menu/bottom_nav_menu” で指定しています。
MyPagerAdapterにも同じ数だけ指定があります。
PagerAdapterとMenuで、順番をそろえる実装方法をとりました。

MainActivity_MyPagerAdapter.java
    private class MyPagerAdapter extends FragmentStateAdapter {
        MyPagerAdapter(FragmentActivity fa) {
            super(fa);
        }

        @Override
        public int getItemCount() {
            return menuList.length;
        }

        int [] menuList = {
            R.id.navigation_pad_2,
            R.id.navigation_piano_2,
            R.id.navigation_midi_2,
            R.id.navigation_config_2,
            R.id.navigation_sponsor_2,
        };
        @Override
        public Fragment createFragment(int position) {
            int newId = menuList[position];
            if (newId == R.id.navigation_pad_2) {
                return NavHostFragment.create(R.navigation.navigation_pad16);
            }
            if (newId == R.id.navigation_piano_2) {
                return NavHostFragment.create(R.navigation.navigation_player);
            }
            if (newId == R.id.navigation_midi_2) {
                return new MidiSelectorFragment(); //ナビゲーションしない場合
            }
            if (newId == R.id.navigation_config_2) {
                //return new ConfigFragment();
                return NavHostFragment.create(R.navigation.navigation_config);
            }
            return new SupportFragment();
        }
    }

new NavHostFragment.create(R.navigation.navigation_player);
この行では、切り替えた部位に、さらにネストしたナビゲーションを配置できます。その実装方法に続きます。

navigation_player.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/navigation_player"
    app:startDestination="@+id/nav_song_select">

    <fragment
        android:id="@+id/nav_song_select"
        android:name="org.star_advance.mixandccle.ui.pianoroll.SongSelectorFragment"
        android:label="@string/hello_song_player"/>

    <fragment
        android:id="@+id/nav_song_pianoroll"
        android:name="org.star_advance.mixandccle.ui.pianoroll.PianorollFragment"
        android:label="@string/hello_song_player"/>

</navigation>

スタートを、SongSelectorFragmentとしていますが、navigationに従ったフラグメントの切り替えは下記で行っています。

SongSelectorFragment.java
public class SongSelectorFragment extends Fragment {

		/* cut some lines */

    public void startStream(InputStream stream) {
        try {
            SMFSequencer seq2 = new SMFSequencer();
            seq2._parser.readFile(stream);
            stream.close();
            PianorollFragment._globalSequencer = seq2;

            NavHostFragment frag = (NavHostFragment) getParentFragment();
            NavHostFragment.findNavController(frag).navigate(R.id.nav_song_pianoroll);
        } catch (IOException e) {
            Log.e(TAG, e.getMessage(), e);
        }
    }
}
PianoRollFragment.java
package org.star_advance.mixandccle.ui.pianoroll;

public class PianoRollFragment extends Fragment {
    static SMFSequencer _globalSequencer = null; //package private

			/* cut some lines */

	    @Override
    public void onResume() {
        super.onResume();

        if (_globalSequencer == null) {
            new Handler(Looper.getMainLooper()).post(() -> {
                MXMixVolume.getInstance().setNoteCountListener(null);
                getParentFragmentManager().popBackStack();
            });
            return;
        }
        if (MXPianoRollSetting._instance._paintKeyReverse) {
            setPianoRollOrientation(true);
        } else {
            setPianoRollOrientation(false);
        }

        if (_globalSequencer.isPlayerRunning() == false) {
            MainParameters params = MainParameters.getInstance();
            _globalSequencer.setMusicBoxMode(params._isMusicBox.getValue());
            _globalSequencer.setTranspose(MXPianoRollSetting._instance._transposePianoroll);
            _globalSequencer.startPlayerThread(0, new SMFCallback() {
                @Override
                public void smfPlayNote(SMFMessage e) {
                    MidiOne.getInstance().startBySequencer(e);
                }

                @Override
                public void smfStarted() {
                    MXMixVolume.getInstance().activateVolumer(true);
                }

                @Override
                public void smfStoped(boolean fineFinish) {
                    MXMixVolume.getInstance().activateVolumer(false);
                    _globalSequencer = null;
                    new Handler(Looper.getMainLooper()).post(() -> {
                        MXMixVolume.getInstance().setNoteCountListener(null);
                        getParentFragmentManager().popBackStack();
                    });
                }

                @Override
                public void smfProgress(long pos, long finish) {

                }
            });

        }
        binding.pianoRoll.setup(_globalSequencer, binding.pianoKeys);
    }

遷移もとでは、この書式ですが、

        NavHostFragment frag = (NavHostFragment) getParentFragment();
        NavHostFragment.findNavController(frag).navigate(R.id.nav_song_pianoroll);

遷移した先から戻る場合は、この書式じゃないと、うまくいかなかったです。2回目の表示でつながりが切れちゃったり、、

        getParentFragmentManager().popBackStack();

まぁこれでうまくいっています。ご参考にしていただけたら、幸いです。

※おまけ 画面を回転させたとき、ナビを下部から左にするコード(上のつづきです)

MainActivity_setupNaviBar.java
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding.getRoot().addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
            setupNaviBar();
        });
        /* cut some lines */
    }
    public void setupNaviBar() {
        if (_stopFeedback > 0) {
            return;
        }
        _stopFeedback ++;
        try
        {
            try {
                int parentWidth = binding.getRoot().getWidth();
                int parentHeight = binding.getRoot().getHeight();

                if (parentWidth == 0 || parentHeight == 0 || binding == null) {
                    new Handler(Looper.getMainLooper()).postDelayed(() -> {
                        setupNaviBar();
                    }, 10);
                    return;
                }

                if (saveWidth == parentWidth && saveHeight == parentHeight) {
                    return;
                }

                saveWidth = parentWidth;
                saveHeight = parentHeight;
                boolean hideTopNavi = false;
                boolean hideSideNavi = false;
                boolean hideBottomNavi = false;

                if (saveWidth > saveHeight) {
                    hideTopNavi = false;
                    hideSideNavi = false;
                    hideBottomNavi = true;
                }else {
                    hideTopNavi = false;
                    hideSideNavi = true;
                    hideBottomNavi = false;
                }
                /*
                if (hideTopNavi) {
                    binding.mainActivityToolbar.getLayoutParams().height = 0;
                }else {
                    binding.mainActivityToolbar.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
                }*/
                if (hideSideNavi) {
                    binding.navSideView.getLayoutParams().width = 1;
                }else {
                    binding.navSideView.getLayoutParams().width = 200;
                }
                if (hideBottomNavi) {
                    binding.navBottomView.getLayoutParams().height = 1;
                }else {
                    binding.navBottomView.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
                }
                binding.getRoot().requestLayout();
            }catch(Throwable ex){
                Log.e(TAG, ex.getMessage(), ex);
            }
        }finally {
            _stopFeedback --;
        }
    }

広告を表示しています。

コメント

タイトルとURLをコピーしました