[Android]スワイプができるユーザーの操作を止めないレビュー誘導を簡単に実装してみた

#はじめに
最速で作れることに重きを置いていたので、最適ではないかもしれませんが早く作りたい方や初心者の方のご参考になればと思い投稿しました。
####この記事の主なターゲット
+ 細かいところはすっ飛ばして、邪魔しないレビュー誘導を実装したい方
+ アニメーションとか座標とか自力で考えるのが難しい、又は面倒だと感じている方
+ レビュー誘導の実装に時間を掛けてらんねぇわって思ってるけど、ユーザーにウザがられたくもないーと感じてる方

#概要(どんな感じのレビュー誘導なん?)
画面のとあるスペースに(あるいはView同士の間に割り込んで)、レビュー誘導を表示します。
こんな感じのレビューですね
<img width="243" alt="スクリーンショット 2020-02-28 14.59.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288250/c3bbd76e-c9f4-31a7-3508-4f8cb2864702.png">

このレビュー誘導をスワイプで削除できるように対応しました。
ユーザーがだるいと思った時にスワイプですぐ消せるのと、いい感じのアニメーションデザインになるためです。
「今回はやめておく」をユーザーがクリックした場合でも、勝手にアニメーションでスワイプさせて削除されるように実装しました。

実際のレビュー誘導はこんな感じです

![wiR0ru7y7ueVg9x2qJSL1582871204-1582871555.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288250/0b1ec83b-9d12-7af7-df28-ced52059593d.gif)

---
###このレビュー誘導の利点
<dl>
  <dt> ・ ダイアログで表示しないのでユーザーからウザがられにくい</dt>
    <dd>
これが結構デカイと思っています。ダイアログで表示されて操作を止められるのは、なかなかイライラしますよね(笑)。ユーザーがイライラした状態でレビューして頂いても、高評価してもらえるのだろうか？って感じです。
    </dd>
</dl>
<dl>
  <dt> ・ Viewの間に割り込んで表示できるので無視されにくくて、かつ操作を止めないので程よいアピール</dt>
    <dd>
ユーザーの操作を止めないのでダイアログじゃないと逆に無視されてしまう可能性があります。<br>しかしこのレビュー誘導はViewの間に割り込んで表示できるので、違和感と邪魔だな感を感じます。本当に邪魔ならスワイプでも消せるし、ユーザーの負担になりにくくアピールをすることができます。
    </dd>
</dl>

#実装(どう作ったん？)
問題は実装です。簡単であれば、すぐみんな使ってます。
スワイプさせたりViewの間に割り込ませたりとか、結構面倒そうだなと感じると思います。
ここをむりくり簡単に実装させますww

簡単に実装させるために、ViewPagerとFragmentStatePagerAdapterとFragmentを利用して実装します。これらを利用したことがあれば、なんとなくどうやってくのか察しがつく方もいらっしゃるんじゃないかと思います。
本来の使い方と全然違うのは重々承知ですw

ViewPager等の本来の使い方は今回は長くなるので省きます。色々なサイトで載ってますのでそちらでお願いします。
まずはFragmentの実装です。

###Fragment

```kotlin:ReviewPromptFragment.kt
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import androidx.fragment.app.Fragment
import com.iwacchi.kakeibo.main.reviewPrompt.OPEN_REVIEWED
import com.iwacchi.kakeibo.main.reviewPrompt.REVIEWED
import com.iwacchi.kakeibo.main.reviewPrompt.ReviewCycleChecker
import com.iwacchi.reviewprompt.R
import com.iwacchi.reviewprompt.preferenceKey.PreferenceUtility

class ReviewPromptFragment : Fragment() {

    companion object {
        fun newInstance(): ReviewPromptFragment {
            return ReviewPromptFragment()
        }
    }

    private var negativeListener: View.OnClickListener? = null
    private lateinit var negativeButton: Button
    private lateinit var positiveButton: Button
    private lateinit var thanksButton: Button
    private lateinit var alreadyReviewedButton: Button

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.review_prompt_layout, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        if(savedInstanceState != null) {
            fragmentManager?.beginTransaction()?.remove(this)?.commit()
        }
        negativeButton = view.findViewById(R.id.negative_button)
        positiveButton = view.findViewById(R.id.positive_button)
        thanksButton = view.findViewById(R.id.thanks_button)
        alreadyReviewedButton = view.findViewById(R.id.already_reviewed_button)

        // アプリレビュー画面に遷移しているかどうか
        if(! PreferenceUtility().getBoolean(OPEN_REVIEWED)) {
            alreadyReviewedButton.visibility = View.GONE
            negativeButton.visibility = View.VISIBLE
            negativeButton.setOnClickListener {
                // reviewサイクルを初期化、サイクル後に再度表示
                ReviewCycleChecker(PreferenceUtility()).resetReviewCycle()
                negativeListener?.onClick(it)
            }
        } else {
            negativeButton.visibility = View.GONE
            alreadyReviewedButton.visibility = View.VISIBLE
            alreadyReviewedButton.setOnClickListener {
                PreferenceUtility().putBoolean(REVIEWED, true)
                thanksMessage(view)
            }
        }
        positiveButton.setOnClickListener {
            openMyAppPlayStore()
            Thread(Runnable {
                Thread.sleep(500)
                Handler(Looper.getMainLooper()).post {
                    thanksMessage(view)
                }
            }).start()
        }
        thanksButton.setOnClickListener {
            negativeListener?.onClick(it)
        }
    }

    private fun thanksMessage(view: View) {
        view.findViewById<TextView>(R.id.title).text =
            getString(R.string.review_prompt_thank_you_message)
        view.findViewById<TextView>(R.id.description).text = ""
        thanksButton.visibility = View.VISIBLE
        positiveButton.visibility = View.GONE
        negativeButton.visibility = View.GONE
        alreadyReviewedButton.visibility = View.GONE
    }

    fun setNegativeListener(listener: View.OnClickListener) {
        negativeListener = listener
    }

    private fun openMyAppPlayStore() {
        PreferenceUtility().putBoolean(OPEN_REVIEWED, true)
        // 変更してください
        val id = "your app package"
        try{
            context?.startActivity(
                Intent(Intent.ACTION_VIEW,
                    Uri.parse("market://details?id=$id"))
            )
        } catch (_: android.content.ActivityNotFoundException) {
            context?.startActivity(
                Intent(Intent.ACTION_VIEW,
                    Uri.parse("https://play.google.com/store/apps/details?id=$id"))
            )
        }
    }

}
```

まぁ普通のFragmentです。PreferenceUtility()というのがちょくちょく出てきてますが、これはSharedPreferenceです。
強いて注目するなら、setNegativeListenerメソッドで「今回はやめておく」のクリック時の処理を追加させています。recreate()の実行時に再生成されることによって、この追加が消えてしまって少し苦労しましたが改善しています。のちに詳しく説明します。

次はAdapterです。

###FragmentStatePagerAdapter

```kotlin:ReviewPromptPagerAdapter.kt
import android.os.Handler
import android.os.Looper
import android.os.Parcelable
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import androidx.viewpager.widget.PagerAdapter
import androidx.viewpager.widget.ViewPager
import com.iwacchi.reviewprompt.reviewPrompt.ReviewPromptFragment

class ReviewPromptPagerAdapter(private val viewPager: ViewPager,
                               fragmentManager: FragmentManager
) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {

    override fun getCount(): Int {
        return 3
    }

    override fun getItem(position: Int): Fragment {
        return when(position) {
            1 -> {
                val fragment = ReviewPromptFragment.newInstance()
                fragment.setNegativeListener(
                    View.OnClickListener {
                        Thread(Runnable {
                            Thread.sleep(100)
                            Handler(Looper.getMainLooper()).post {
                                viewPager.setCurrentItem(0, true)
                            }
                        }).start()
                    }
                )
                fragment
            }
            else -> {
                Fragment()
            }
        }
    }

    override fun getItemPosition(`object`: Any): Int {
        return if(`object` is ReviewPromptFragment)
            PagerAdapter.POSITION_NONE
        else
            PagerAdapter.POSITION_UNCHANGED
    }

    override fun saveState(): Parcelable? {
        return null
    }

}
```

今回のミソはここかなと思います。注目するとこは二つあります。

まず、一つ目はメインのレビュー誘導のReviewPromptFragmentのPositionを1番目、空のFragmentのPositionを0番目と2番目に置きます。そして、先ほど言及したClickListenerをgetItem()のところでReviewPromptFragmentに追加させていきます。
内容は0番目のFragmentに飛ばす処理です。これで勝手にスワイプします。スワイプで消える処理はViewPagerに記載します。

二つ目は、recreateへの対応です。これは何かというと、なんらかのタイミングでrecreateが実行された場合に、Fragmentが再生成されます。しかし、fragmentManagerで保持したままでそれぞれのFramgent自身で再生成してしまうため、普通に実装してると、recreateされたときにgetItemが実行されないのです。
それを防ぐために、下のgetItemPosition()とsaveState()を追加しています。この二つの追加で再生成時に保持していたfragmentを削除して再生成してくれます。

次はviewpagerです。

###ViewPager

```kotlin:ReviewPromptViewPager.kt
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.Transformation
import androidx.fragment.app.FragmentManager
import androidx.viewpager.widget.ViewPager
import com.iwacchi.reviewprompt.reviewPrompt.ReviewPromptPagerAdapter

class ReviewPromptViewPager(context: Context,
                            attributeSet: AttributeSet) : ViewPager(context, attributeSet) {

    fun setAdapter(fragmentManager: FragmentManager) {
        this.adapter =
            ReviewPromptPagerAdapter(
                this,
                fragmentManager
            )
        this.currentItem = 1
        changeListener()
    }

    private fun changeListener() {
        this.addOnPageChangeListener(
            object : OnPageChangeListener {
                override fun onPageScrollStateChanged(state: Int) {}
                override fun onPageScrolled(
                    position: Int,
                    positionOffset: Float,
                    positionOffsetPixels: Int) {}
                override fun onPageSelected(position: Int) {
                    when(position) {
                        0, 2 -> {
                            deleteReviewPrompt()
                        }
                    }
                }
            }
        )
    }

    fun deleteReviewPrompt() {
        val animation = ResizeAnimation(
            this,
            - this.height,
            this.height
        ).apply{
            duration = 200
        }
        Thread(Runnable {
            Thread.sleep(200)
            Handler(Looper.getMainLooper()).post {
                this.startAnimation(animation)
            }
            Thread.sleep(200)
            Handler(Looper.getMainLooper()).post {
                (this.parent as ViewGroup).removeView(this)
            }
        }).start()
    }

    class ResizeAnimation(private val view: View,
                          private val addHeight: Int,
                          private val startHeight: Int) : Animation() {

        override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
            val newHeight = startHeight + addHeight * interpolatedTime
            view.layoutParams.height = newHeight.toInt()
            view.requestLayout()
        }

        override fun willChangeBounds(): Boolean {
            return true
        }

    }

}
```

adapterをセットさせた時にcurrentItemを1にするのを忘れないでください。あとはページ変更で0番目と2番目にスワイプさせた時に自身のView自体をremoveさせて軽くさせます。
removeさせる前にResizeAnimationでアニメーションを加えています。heightを0にアニメーションさせた後に削除させる感じですね。この時のdurationも合わせるように設定してください。
以上でレビュー誘導の実装の完成です。
だいぶ楽です。

```kotlin:MainActivity.kt
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.ViewGroup
import com.iwacchi.reviewprompt.preferenceKey.PreferenceUtility
import com.iwacchi.reviewprompt.reviewPrompt.ReviewPromptViewPager

class MainActivity : AppCompatActivity() {

    private lateinit var reviewPromptViewPager: ReviewPromptViewPager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        reviewPromptViewPager = findViewById(R.id.review_prompt_view_pager)

        // レビュー誘導を表示させるかどうかの判定
        val check = true
        if(check){
            // レビュー誘導をセットする
            reviewPromptViewPager.setAdapter(supportFragmentManager)
        } else {
            // レビュー誘導のViewを削除しちゃう(View.GONEとかではなく完全削除)
            (reviewPromptViewPager.parent as ViewGroup).removeView(reviewPromptViewPager)
        }
    }
}

```

こんな感じですぐ使えます。レイアウトはこんな感じです。

```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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Hello World!!"
        android:gravity="center"
        app:layout_constraintBottom_toTopOf="@+id/review_prompt_layout"/>

    <LinearLayout
        android:id="@+id/review_prompt_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent">

        <ReviewPromptViewPager
            android:id="@+id/review_prompt_view_pager"
            android:layout_width="match_parent"
            android:layout_height="150dp" />

    </LinearLayout>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Hello World!!"
        android:gravity="center"
        app:layout_constraintTop_toBottomOf="@+id/review_prompt_layout"/>

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

レイアウトの注意点ですがLinearLayoutかなんかで囲ってください。removeさせてViewそのものを消してしまうので、レイアウトが崩れてしまいます。

#おわりに
無理くりスワイプできるレビュー誘導を紹介しました。レビュー誘導実装したいけどダイアログでやるのもイマイチ...
手っ取り早くいい感じのを作りたいといった方は是非利用してみてください。

全体のコードはこちらに載せているのでコピペなりして使っていただければと思います。
[https://github.com/iwacchi/ReviewPrompt](https://github.com/iwacchi/ReviewPrompt)

---
###参考文献

この記事を投稿する際に、以下のサイトで勉強させていただきました。

-【Android】アニメーション付き開閉Viewの作り方-
[https://qiita.com/farman0629/items/ed86059845551449a359](https://qiita.com/farman0629/items/ed86059845551449a359)

-Androidで自身のアプリのPlay Storeを開く-
[https://qiita.com/jumperson/items/74992484e8acb758a213](https://qiita.com/jumperson/items/74992484e8acb758a213)