読者です 読者をやめる 読者になる 読者になる

ListViewをRecyclerViewに置き換えるにあたっての注意メモ(1)

はじめに

RecyclerViewでしかできない操作を実装するのではない限り、

無理してListViewから置き換える必要はないです

そもそも TwitterKit とかでも普通に ListView 使っていますし。。

どういう場合に置き換えるのか?

  • Gmail横スワイプでメール削除するような操作を作りたい時
  • リスト内でD&D並び替えみたいないことをしたい時
  • GridViewを置き換えたい時
    • あとで記載しますが、正直な所 GridLayoutManager で置き換えるより support.v7.widget.GridLayout を使うほうが楽です

こういう用途なら無理して置き換える必要がないです

古いアプリのちょっとしたマテリアル化

compile ‘com.android.support:design:25.1.0’

辺り入れて

  • SnackBar
    • Toastの代わりに*1
  • BottomSheetDialog

    • RecyclerViewのサンプル多いけど、実はListViewでも問題なく動きます
  • Android Material Design Icon Generator で G様のMaterial Design Iconの利用に変更

  • Android Drawale Viewer でdpiごとの抜けの確認
  • mipmapでOS4.3以降でアイコンをきれいに表示する対応*2

とかを使う場合。

引っかかる的な話

上位から要素データを引数で渡してAdapter内でデータ操作してしまうと、データが変更されてしまう

RecyclerView自体が ListViewよりかなり薄くて軽量なクラスというのが原因

ListViewのときは、ArrayAdapter等の内部で自動的にdeep複製されているようなのですが、

RecyclerViewだと

  • ListView
public class ImageAdapter extends ArrayAdapter<ListItem> {
        public ImageAdapter(Context context, List<ListItem> objects) {
            super(context, 0, objects);
            mInflater = (LayoutInflater) context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }
  • RecyclerView (NG)
public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder> {
    private List<ListItem> mDataSet = new Arraylist<>();
    public ImageAdapter(Context context, List<ListItem> myDataSet) {
        mDataSet.addAll(myDataSet);//★
        mInflater = (LayoutInflater) context
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

参照はいわずもがな、★のようにしても駄目。自前でdeepCopyしないと駄目なんでしょうね。。

  • RecyclerView (OK)
public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder> {
    private final LayoutInflater mInflater;
    private List<ListItem> mDataSet = new Arraylist<>();
    public ImageAdapter(Context context) {
        mInflater = (LayoutInflater) context
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }
    
//== 新規追加 === 
    public void addAll(List<ListItem> myDataSet){
        mDataSet.addAll(myDataSet);//★
        notifyDataSetChanged();
    }
  • 完全に表示用のデータとして割り切る。
    • 表示用のListは内部だけで完結する
  • データ操作はAdapter内部でするようなコードは書かない。
    • clickのinterfaceとかはActivity/Fragmentレイヤーまで押し出すべき

というのが混乱しないベストプラクティスなのかと

行クリックができない

public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder> {
    private OnItemClickListener mListener;

    private final LayoutInflater mInflater;
    private List<ListItem> mDataSet = new Arraylist<>();
    public ImageAdapter(Context context) {
        mInflater = (LayoutInflater) context
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, final int position) {
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            final ListItem item = mDataSet.get(position);
            @Override
            public void onClick(View v) {
                if(mListener != null){
                    mListener.onItemClick(ImageAdapter.this,position,item);
                }
            }
        });
    }

    public void setOnItemClickListener(OnItemClickListener listener) {
        mListener = listener;
    }
    
    public static interface OnItemClickListener {
        public void onItemClick(ImageAdapter adapter, int position, ListItem item);
    }
}

らへんの話。

利便性を上げるための工夫

  • ArrayAdapter であったデータ操作関数
  • RecyclerView.Adapter に追加する

    • ただし、マルチアクセスするとエラーになるので自前でsyncronizedする
  • getCount

  • add
  • remove
  • getItem
  • addAll
  • clear
  • sort
  • reverse

辺りは用意しておくと楽になるかと思います

public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder> {
        private List<ListItem> mDataSet = new Arraylist<>();
    private Object lock = new Object();

//== 自前追加 === 

public int getCount(){
    return getItemCount();
}

public int getItem(int position){
    return mDataSet.get(position);
}

public void remove(ListItem item){
    synchronized(lock){//★
        int positon = mDataSet.indexOf(item)
        mDataSet.remove(position);
        notifyItemRemoved(position);
    }
}

public void add(ListItem item){
    synchronized(lock){//★
        mDataSet.add(item);
        notifyItemInserted(getCount());
    }
}

public void sort(Comparator<ListItem> comparetor ){
    synchronized(lock){//★
        Collections.sort(mDataSet,comparetor);
        notifyDataSetChanged();
    }
}

public void reverse(){
    synchronized(lock){//★
        Collections.reverse(mDataSet);
        notifyDataSetChanged();
    }
}

public void clear(){
    clear(false);
}

public void clear(boolean refresh){
    synchronized(lock){//★
        mDataSet.clear();
        if(refresh)notifyDataSetChanged();
    }
}

public void addAll(List<ListItem> myDataSet){
    synchronized(lock){//★
        mDataSet.addAll(myDataSet);
        notifyDataSetChanged();
    }
}

Adapterクラスで使うitemのレイアウトが縮む

  • ListViewでは問題なかった R.layout.list_view_item なレイアウト

が表示がよく崩れることが有り。

  • ListView時
    • LinerLayout
  • RecyclerView
    • RelativeLayout

辺りに変更しないと、中央に表示内容が寄ってしまう みたいな感じになったりも

ココらへんlayout previewでは確認できないので、

InstantRun頼みで繰り返し実機確認前提みたいな状況が、正直辛かったりします

GridLayoutManagerを使う場合の問題点

  • 縦か横のカラム数しか指定できない
  • 指定しなかったほうは、layoutの指定、Viewの大きさ等に依存してしまう
    • したがって 横3、縦1の長い表示になったり
    • Instagramみたいな正方形タイルを敷き詰めるみたいなのを作ろうとした場合、もうひと工夫が必要

の話を例にすると

onBindViewHolderのタイミングで

  • 横displayサイズ / 横column 3 で割る or 横サイズを指定する
  • その値を縦サイズに指定する
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
   // attach data to covertView
   ListItem item = mDataSet.get(position);
   holder.imageview.setImageResource(item.img_id);

   //スクリーンサイズの1/3を取得
   int step_w = DisplayUtils.getDisplayWidthPixels(mContext) / 3;

   View v = holder.itemView;
   ViewGroup.LayoutParams params = v.getLayoutParams();
   params.width = step_w;  //◎
   params.height = step_w; //◎ 縦横を一致させる
   v.setLayoutParams(params);
}

onCreateViewHolder でもいいという記載もあった気がしますが、★の指定をしてしまうと

これ最終的にinfrate先のレイアウトに依存するので表示がずれるという話があった気がする*3

<android.support.v7.widget.RecyclerView
    android:id="@android:id/list"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" //★
    android:clipToPadding="false"/>
public class DisplayUtils {
    public DisplayUtils() {
    }

    public static int pixelFormat(Context context, int dp) {
        float density = context.getResources().getDisplayMetrics().density;
        int px = (int)((float)dp * density + 0.5F);
        return px;
    }

    public static int getDisplayWidthPixels(Context context) {
        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
        return metrics.widthPixels;
    }

    public static int getDisplayHeightPixels(Context context) {
        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
        return metrics.heightPixels;
    }

    public static int getDisplayStatusBar(Context context) {
        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
        return (int)Math.ceil((double)(25.0F * metrics.density));
    }
}

下記あたりも絡む話

qiita.com

*1:でもこれ表示する瞬間にbackkeyとかで戻るとクラッシュする

*2:下記のページでは4.2からとなっているようですが、他のサイトの記載とか調べると、どうも4.3からっぽい

*3:ここらへんsupport-libraryの23.2辺りの仕様変更だった気がするので、仕方ないのかも