Android RecyclerView & CardViewのonClickイベントにハマる

昨日、RecyclerViewのクリックイベントがどうしても取得できずハマってしまったのでメモしておきます。

RecyclerView onClickイベントの基本系

Layout

RecyclerViewの中のitemを以下のようなレイアウトにした場合、

<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:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="wrap_content">

    <android.support.v7.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/cardView">
    </android.support.v7.widget.CardView>
</LinearLayout>
onClickイベント

基本は RecyclerView.AdapterのonCreateViewHolder()の実装にonClickイベントを書くことになると思う。ネット上の情報もほぼこれ。

override fun onCreateViewHolder(parent: ViewGroup, p1: Int): ViewHolder {
    val itemView = LayoutInflater.from(parent.context).inflate(R.layoute.card_view_layout, parent,false)
    val holder = ViewHolder(itemView)
    //itemViewにリスナーを設定
    holder.itemView.setOnClickListener {
        //クリックされたViewHolderの位置が取得できる
        val position = holder.getAdapterPosition();
        doAction(piosition)
    }
    return holder
}

ただし、CardViewのクリック時に選択されたことを表現する為にandroid:clickable="true"を定義した場合、上記で言うitemViewのonClickイベントが取得できなくなります。

    <android.support.v7.widget.CardView
            android:clickable="true"
            android:foreground="?android:attr/selectableItemBackground"
            ...

CardView OnClickの定義

CardViewでリスナーの定義をするには単純に

override fun onCreateViewHolder(parent: ViewGroup, p1: Int): ViewHolder {
    val itemView = LayoutInflater.from(parent.context).inflate(R.layoute.card_view_layout, parent,false)
    val holder = ViewHolder(itemView)
    //CardViewにリスナーを設定
    holder.itemView.findViewById<CardView>(R.id.cardView).setOnClickListener {
        val position = holder.getAdapterPosition();
        doAction(piosition)
    }
    return holder
}

とすればよい。

蛇足 

上記のdoActionはAdapter内にベターっと書かずにActivityやFragment側で記述するのがシャレオツみたい。

interfaceを定義してうけとればよい。

Adapter

class FooAdapter( var mListener:onCardViewItemClick ): RecyclerView.Adapter<ViewHolder>(){
    interface onCardViewItemClick{
        fun onClick(position: Int)
    }
    override fun onCreateViewHolder(parent: ViewGroup, p1: Int): ViewHolder {
        ...中略
        holder.itemView.findViewById<CardView>(R.id.cardView).setOnClickListener {
            val position = holder.getAdapterPosition();
            mListener.onClick(piosition)
        }
        return holder
    }
}

Fragment側で実装を渡そう

class BarFragment : Fragment(),
    FooAdapter.onCardViewItemClick{
    override fun fun onClick(position: Int){
        //doAction
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...略
        recyclerView.adapter = FooAdapter(tihs)
    }
}

Android ViewのZ軸、重なりの高さを管理する

RecyclerView & CardViewの上にprogressBarを表示しようとした時、CardViewが最前面に表示されprogressBarがCardViewの下に潜ってしまうケースがある。

ViewのZ軸高さの設定

android:elevationでZ軸の高さを設定する。他Viewにandroid:elevationの設定がない場合、設定を行ったViewが最前面に表示される。

<ProgressBar
        style="?android:attr/progressBarStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:elevation="2dp">

Android FragmentPagerAdapterにセットされたFragmentにアクセスする

例えばActivity側で定義されたActionBarやToolBarのUIで操作が行われた時、ViewPager側のFragmentを操作する必要がある時等に。

Activity側

FragmentPagerAdapter.getItem(position)でFragmentを取得すればよい。positionはViewPager.currentItemで取得できる。

val viewPager =  findViewById<ViewPager>(R.id.viewPager)
val mFragmentPagerAdapter =  object : FragmentPagerAdapter(supportFragmentManager){  ... 略 }
...
val i = viewPager.currentItem
//currentのfragmentを取得
val fragment = mFragmentPagerAdapter.getItem(i) as ViewPagerFragment
//fragmentにアクセス
fragment.doAction()
Fragment側

Fragment側はActivityから実行されるメソッドをインターフェースとしておけばアクセスインスタンスを抽象化できる

interface ViewPagerFragment {
    fun doAction()
}
Foo : Fragment(), ViewPagerFragment{
    override fun doAction(){}
}

ただし、FragmentPagerAdapter.getItem(position)で取得したFragmentの状態によって、メンバで保持してるインスタンス等破棄されているケースがある。特にlateinit var のメンバにアクセスしたりするとぬるぽになる時があるので注意しよう。

Android ネットワークAPIから取得した情報をキャッシュする

軽量なJsonデータなどの文字列情報はSharedPreferencesを使って保存するのが便利

SharedPreferencesにアクセス
fun getCache(context: Context, key:String):String{
    val cache = PreferenceManager.getDefaultSharedPreferences(context).getString(key, "")
     return cache
}

fun createCache(context: Context, key: String, value:String){
    PreferenceManager.getDefaultSharedPreferences(context).edit().apply {
        putString(key, value)
        apply()
    }
}
使用例
val cache_key = "response"
var str = ""
if( isNetworkAvailable(context) ){
    str = //APIから取得する処理
    createCache(context, cache_key, str )
}
//オフライン時は前回取得のデータを返す
else{
    str = getCache(context, cache_key)
}

これでオンライン時には新しい情報にキャッシュを更新し、オフライン時には前回取得したキャッシュを参照できる。

Android オフライン時にWebViewキャッシュを参照する

オフラインであるかの判定は前回の記事、

www.letitride.jp

isNetworkAvailable()を使用して取得しています。

WebViewキャッシュまわりの設定

状態によりcacheModeを切り替えればよい。

val webView = findViewById<WebView>(R.id.webView)
webView.settings.setAppCachePath(applicationContext.cacheDir.absolutePath)
webView.settings.allowFileAccess = true
webView.settings.setAppCacheEnabled(true)
webView.settings.javaScriptEnabled = true
//基本はネットワークから参照する
webView.settings.cacheMode = WebSettings.LOAD_DEFAULT
//オフライン時はキャッシュ取得を試みる
if ( isNetworkAvailable(context) ) { // when offline
    webView.settings.cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK
}
webView.loadUrl( url )

また、layoutにWebViewを配置せずとも

val webView = WebView(context)
//...中略
//キャッシュがある場合は通信しない
webView.settings.cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK
webView.loadUrl( url)

とコード上でWebViewを生成することで、urlを先読みしてキャッシュを構築することが出来る。

生成したキャッシュを読むことで、オフライン時にはキャッシュされたWebページを表示することが可能。

Android 端末のネットワーク状態を取得する

端末がオンライン / オフラインであるかの確認

AndroidManifestのパーミッション追加

AndroidManifest.xml

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
ネットワーク状態のオンオフ判定
fun isNetworkAvailable(context: Context): Boolean {
    val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
    if(connectivityManager == null ){ return false }
    if(connectivityManager.activeNetworkInfo == null ){ return false }
    return connectivityManager.activeNetworkInfo.isConnected
}