うきっとラボ~中卒から始めるプログラミング~

中卒のポンコツ太郎が立派なプログラマになるまでの道のり

【Android】ViewPagerでスライドショーを実現する

使用するクラス

MainActivity スライドショーを表示するアクティビティ
SlideFragment1 スライド1ページ目を表すフラグメント
SlideFragment2 スライド2ページ目を表すフラグメント
SlideFragment3 スライド3ページ目を表すフラグメント
SlidePagerAdapter ViewPager(後述)のアダプターにセットするAdapterクラス


それぞれのActivity、Fragmentに対応するレイアウトも使用します(∩´∀`)∩




表示したいレイアウトにViewPagerを持たせる

ViewPagerは、スライドショーを実装するために便利なViewです(/・ω・)/


デフォルトでページを切り替えるための画面スライドアニメーションが表示されるため、独自のアニメーションを作成する必要はありません。


では、MainActivityにViewPagerを持たせます。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="10dp"
        android:layout_marginBottom="10dp"
        android:text="スライドショー"
        android:textSize="30dp" />

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/vpSlide"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ffb6c1"
        android:layout_marginBottom="10dp"/>

</LinearLayout>

f:id:ukiuki0518:20200214140459j:plain
ピンク色のエリアがViewPager




「1ページ分」を表す各フラグメントを作る

次は、先ほどのViewPagerエリアに表示される、各ページを表すフラグメント1ページずつ作っていきます。


ViewPagerは、フラグメントをスライドアニメーションに合わせて次々に生成してエリアに表示させるため、
それぞれのページに対応したフラグメントが必要、ということですね(^_^)


例えば3ページある場合、3つのフラグメントを作ることになります。


(ただし、条件によってはそうしなくてもよい場合もあります。これは後述しますね!)


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/img_slide_1"/>

</LinearLayout>

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/img_slide_2"/>

</LinearLayout>

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/img_slide_3"/>

</LinearLayout>

今回はシンプルに画像を表示するだけのレイアウトにしました('ω')



では、フラグメントクラスを作っていきます。


もちろん個別にファイルを作ってもいい(それが一般的?)と思うのですが、
なんかごちゃごちゃして嫌だったので1ファイルにまとめてみました(;'∀')

class SlideFragment1: Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? = inflater.inflate(R.layout.fragment_slide_1, container, false)
}

class SlideFragment2: Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? = inflater.inflate(R.layout.fragment_slide_2, container, false)
}

class SlideFragment3: Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? = inflater.inflate(R.layout.fragment_slide_3, container, false)
}

各フラグメントクラスでは、レイアウトをView化してそのまま返していますね(∩´∀`)∩




Adapterクラスを作る

さて、これまでの作業で

表示するエリアの確保(ViewPager)

表示するページの作成(Fragment)

ができたわけですが、


当然これだけでは表示されません(;'∀')


ListViewRecylerViewではAdapterを使ってViewとデータの橋渡しを行いますよね(/・ω・)/
それと同じように、ViewPagerでもAdapterを使用するわけです!


今回はFragmentStateAdapterを継承して独自のAdapterクラスを作成していきましょう。


FragmentStateAdapter
フラグメントを使用して各ページを管理するPagerAdapterを継承しており、
フラグメントの状態の保存と復元も処理できるアダプターです(^_^)


class SlidePagerAdapter(fm: FragmentManager, private val list: List<Fragment>) :
    FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {

    override fun getItem(position: Int): Fragment {
        return list[position]
    }

    override fun getCount(): Int {
        return list.size
    }
}



<ポイントその1>

親クラス(FragmentStateAdapter)のコンストラクタに渡す値


①自身のクラスのコンストラクタに渡されたFragmentManager
②フラグメントのライフサイクルの挙動を表す定数


以前は①だけのコンストラクタを使用することもできていたようですが、
現在は非推奨となっており、②は必須となります!


②の定数は2種類あるようです。

BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 現在のフラグメントのみがLifecycle.State.RESUMED状態になり、 他のすべてのフラグメントは、Lifecycle.State.STARTEDで制限されます。
BEHAVIOR_SET_USER_VISIBLE_HINT すべてのフラグメントはLifecycle.State.RESUMED状態になり、現在のフラグメントが変更されたときFragment.setUserVisibleHint(boolean)へのコールバックがあります。非推奨


ただし、BEHAVIOR_SET_USER_VISIBLE_HINTは現在非推奨になっているため、実質指定できるのはBEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENTのみです。



<ポイントその2>
2つのオーバーライドメソッド

getItem(position: Int) 表示するページとなるFragmentを返します。
getCount() リストのサイズを返します。

画面に表示させる

最後にやることは3つです。

ページを表すフラグメントのリストを生成
独自アダプターにデータを渡してオブジェクト生成
ViewPagerにアダプターをセット


では、コードを見てみましょう

class MainActivity : AppCompatActivity() {

    private lateinit var _list: List<Fragment>
    private lateinit var _adapter: SlidePagerAdapter

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)


        //ページを表すフラグメントのリストを生成
        _list = mutableListOf(
            SlideFragment1(),
            SlideFragment2(),
            SlideFragment3()
        )

        //独自アダプターにデータを渡してオブジェクト生成
        _adapter = SlidePagerAdapter(supportFragmentManager, _list)

        //ViewPagerにアダプターをセット
        vpSlide.adapter = _adapter

    }

}

これで完成です!!


では、画面を見てみましょう!




完成

f:id:ukiuki0518:20200214155542g:plain:h400
スライドショー完成!

見栄えの関係でピンク色のエリアは元に戻しています(∩´∀`)∩




実験~コンパクトにしてみた~

今回のように各ページの変化が「1つの画像リソースだけ」のような場合は、

もう少しコンパクトになる気がします。


どういうことかというと、

「リソースだけ動的に変えながら1つのフラグメントクラスから複数インスタンス生成したらいんでね?」

ってことです('ω')‼


ってことで
ちょっくら実験してみました。


いじるのはフラグメントレイアウトと、MainActivity.ktSlideFragments.ktです。


まずはレイアウトから。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/ivSlide"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

レイアウトをこれ1つにして、

「fragment_slide_2.xml」と「fragment_slide_3.xml」は削除しました('ω')ノ



次に、フラグメントクラス

class SlideFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        val imgResource = arguments?.getInt("imgResource")

        val view = inflater.inflate(R.layout.fragment_slide, container, false)
        val ivSlide: ImageView = view.findViewById(R.id.ivSlide)

        try {

            ivSlide.setImageResource(imgResource!!)

        } catch (e: Exception) {
            when(e){
                is Resources.NotFoundException,
                is NullPointerException ->{
                    e.printStackTrace()
                    ivSlide.setImageResource(R.drawable.img_error)
                }
                else ->{
                    throw e
                }
            }
        }
        
        return view

    }
}

レイアウト同様、フラグメントクラスもこれ一つにして、これ以外は削除しました。


コンストラクタでリソースIDを受け取り、
ImageViewの画像リソースだけを動的に変更して返すコードにしました。

<ーー修正ーー>
AndroidDeveloperによるとFragmentのコンストラクタは空でないとNGとのことです(;_;)…

代わりに、Argumentから値を受け取るコードに修正しました(;'∀')
<-ー---->

受け取ったリソースIDはIntなので、変な値が渡された時の対処としてエラー用の画像を用意。これを表示するようにしています。



最後にMainActivity

class MainActivity : AppCompatActivity() {

    private lateinit var _list: List<Fragment>
    private lateinit var _adapter: SlidePagerAdapter

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        _list = listOf(
            createNewSlideFragment(R.drawable.img_slide_1),
            createNewSlideFragment(R.drawable.img_slide_2),
            createNewSlideFragment(R.id.ivSlide)
        )

        _adapter = SlidePagerAdapter(supportFragmentManager, _list)

        vpSlide.adapter = _adapter

    }

    private fun createNewSlideFragment(imgResource: Int): SlideFragment {
        val fragment = SlideFragment()
        val args = Bundle()
        args.putInt("imgResource", imgResource)
        fragment.arguments = args
        return fragment
    }

}

リソースをArgumentsにセットしたフラグメントものを返すcreateNewSlideFragment()メソッドを作成しました('ω')ノ

これによって生成されたフラグメントを、リストに格納しています(∩´∀`)∩



また実験として、3つ目のインスタンスのコンストラクタ引数にはエラーになるような値を渡しています。



どうなるでしょうか?

f:id:ukiuki0518:20200214162524g:plain:h400
ちゃんとできました。


コンパクトになりましたね(∩´∀`)∩


全てのパターンに当てはまるわけではないですが、
大まかなレイアウトが同じなら、ある程度応用できそうです(/・ω・)/


表示するページ数があらかじめ決まっている場合
個別にレイアウトと対応クラスを作ってもいいと思いますが、


特に動的にページ数を変えたいなんて時には、
こちらの方法を使うとよいかもしれません(^_-)-☆


追記・修正

2020/2/14
ー 実験~コンパクトにしてみた~ にて、Fragmentのコンストラクタに値を渡していたため修正。
修正前のコード

class SlideFragment(private val imageId: Int): Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        val view = inflater.inflate(R.layout.fragment_slide, container, false)
        val ivSlide: ImageView = view.findViewById(R.id.ivSlide)

        try {
            ivSlide.setImageResource(imageId)
        }catch (e: Resources.NotFoundException){
            e.printStackTrace()
            ivSlide.setImageResource(R.drawable.img_error)
        }

        return view

    }
}

class MainActivity : AppCompatActivity() {

    private lateinit var _list: List<Fragment>
    private lateinit var _adapter: SlidePagerAdapter

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        _list = listOf(
            SlideFragment(R.drawable.img_slide_1),
            SlideFragment(R.drawable.img_slide_2),
            SlideFragment(R.id.ivSlide) //←実験
        )

        _adapter = SlidePagerAdapter(supportFragmentManager, _list)

        vpSlide.adapter = _adapter

    }

}