Recyclerviewの実装をしてみた。+ 要素の一番上、一番下を判定を行う

ListViewばっかり使用していましたが、置き換えるついでに色々やってみました。

## 導入
build.gradleに以下を。（1.2.1は記事を書いている当時の最新なので読み替えてください。）
参考：[Recyclerview](https://developer.android.com/jetpack/androidx/releases/recyclerview?hl=ja)

``` build.gradle
dependencies {
    implementation 'androidx.recyclerview:recyclerview:1.2.1'
}
```

## 実装

とりあえずザーッと。
（細かい部分はよしなに・・・）

#### 表示する要素のレイアウト
各要素は全て以下を基準に作られます。

``` item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/sample_text"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

```

#### Holderクラス

上記記載の表示する要素のレイアウト（item.xml）の要素の参照を保持します。

``` SampleViewHolder.kt
class SampleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val view: TextView = itemView.findViewById(R.id.sample_text)
}
```

#### Adapterクラス
基本的にはこれにお任せ。

onCreateViewHolder()、onBindViewHolder()、getItemCount()の3つをoverrideする必要があります。

getItemCount()で返却した個数分要素を作成してくれます。
作成した要素に色々設定するのはonBindViewHolder()の仕事です。



``` SampleAdapter.kt
class SampleAdapter(private val items: List<String>) : RecyclerView.Adapter<SampleViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SampleViewHolder {
        val itemView: View =
            LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false)
        return SampleViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: SampleViewHolder, position: Int) {
        holder.view.text = items[position]
    }

    override fun getItemCount(): Int {
        return items.size
    }
}
```

他にはgetItemViewType()なんかもoverrideすることができます。
positionがInt型で渡されるので、何番目の場合はこのviewHolder、とかを返せそうですね。

``` SampleAdapter.kt
override fun getItemViewType(position: Int): Int {
    return if(position == 0){
        // headerに紐づくIntを返す
        HEADER
    } else {
        // 通常itemに紐づくIntを返す
        NORMAL_ITEM
    }
}
```

上みたいなことをやった後に、

``` SampleAdapter.kt
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    return if(viewType == HEADER) {
        // RecyclerView.ViewHolderを継承したHeaderViewをinflateしてholderに設定
        val headerView: View =
            LayoutInflater.from(parent.context).inflate(R.layout.header, parent, false)
        HeaderViewHolder(headerView)
    } else {
        // RecyclerView.ViewHolderを継承した通常ItemViewをinflateしてholderに設定
        val normalItemView: View =
            LayoutInflater.from(parent.context).inflate(R.layout.normal_item, parent, false)
        NormalItemViewHolder(normalItemView)
    }
}
```

みたいなことをやればRecyclerviewの先頭の要素をHeader（任意の別のview）にすることができそうです。
この場合はAdapterが継承しているRecyclerView.Adapterの型や、overrideしたメソッドの引数の型など諸々がRecyclerView.ViewHolderになるのでご注意を。
あとはgetItemCount()が返す値をitems.size + 追加要素数（headerだけなら+1）にすること、onBindViewHolder()で引数のholderがHeaderViewHolderなのか、NormalItemViewHolderなのかを確認してから操作するのをお忘れなく。


#### Activity（利用View）

``` activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/sample_recycler_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

``` MainActivity.kt
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val items = mutableListOf<String>()

        // 1番目〜100番目までになります。
        for (i in 1..100) {
            items.add(i.toString() + "番目")
        }

        // layoutManagerとadapter、scrollListenerを設定してあげる。
        binding.sampleRecyclerView.apply {
            layoutManager = LinearLayoutManager(context)
            adapter = SampleAdapter(items)

            // ここからが今回のテーマ
            addOnScrollListener(object : RecyclerView.OnScrollListener() {
                override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                    super.onScrolled(recyclerView, dx, dy)

                    // 一番上に到達（縦方向（マイナス方向）へのスクロールができない）
                    if (!recyclerView.canScrollVertically(-1)) {
                        // 何かしらの処理
                    }

                    // 一番下に到達（縦方向（プラス方向）へのスクロールができない）
                    if (!recyclerView.canScrollVertically(1)) {
                        // 何かしらの処理
                    }
                }
            })
        }
    }
}

```
題名にあった一番上、一番下の判定はこちらです。
今回はRecyclerviewのOnScrollListener#onScrolledでY軸にそれ以上進めないことの判定をしています。

その他の判断方法としては、以下を使用すると色々できそうです。

``` sample.kt
// 画面に表示されているうち、最初のitemのposition取得
val firstVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()

// 画面に表示されているitemの個数
val visibleItemCount = recyclerView.childCount
// Recyclerviewに設定されているitemの個数（表示・非表示に関わらない）
val itemCount = (recyclerView.layoutManager as LinearLayoutManager).itemCount

``` 
・画面に表示されている一番上の要素が0番目であること
・画面に表示されている一番上の要素の位置と表示個数を合わせた（位置を個数分ずらした）ものが総数と同じこと
とかでしょうか・・・。
findFirstVisibleItemPosition()は、ほんの少しでも表示された瞬間切り替わるので気をつけてください。



まだまだ勉強不足感があります・・・。精進します。
以上です。