Help us understand the problem. What is going on with this article?

【Android9.0 Pie Java】チャットアプリで横スワイプ→ダイアログ表示→削除を実装する

環境

Android9.0 Pie Java

はじめに

最近Android Javaでチャットアプリを作成していまして、LINE風のチャット一覧削除を実装しました。
RecyclerViewのリスト表示と削除機能の記事は結構ありますが、

横スワイプ→ダイアログ表示→削除
MainActivity + 複数のFragment構成

の実装例は日本語の記事では見つからなかったので共有させて頂きます。
(ベストプラクティスかは怪しいので参考程度にお願いします。。ご指摘大歓迎です!)

こんな実装ができます
243e5a5ccb079ad0d385004d69a930ba.gif

完成品URL

非常にコンテンツが長いので、完成品を取り合えず手に入れたい方はこちらからどうぞ。
https://github.com/yuta-matsumoto/chat

ディレクトリ構成

ビルドファイルやマニフェストファイルは省略します

Chat
 ├app/src/main/
      ├java/
      │ └com.example.chat/
      │         ├fragments/
      │         │    ├BaseFragment.java
      │         │    ├ChatListFragment.java
      │         │    └DeleteChatFragment.java
      │         ├helpers/
      │         │    ├ChatListAdapter.java
      │         │    ├ViewHolder.java
      │         │    └SwipeHelper.java
      │         ├models/
      │         │    ├ChatRowData.java
      │         │    └DeleteChatRow.java
      │         └MainAcitivity.java
      ├res/
      │ ├drawable/
      │ │   └sample1.png(以下省略)
      │ ├layout/
      │ │   ├activity_main.xml
      │ │   ├chat_list_row.xml
      │ │   └fragment_chat_list.xml
      │ └values/
      │     ├colors.xml
      │     ├strings.xml
      │     └styles.xml

コード

依存関係

build.gradle
dependencies {
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    implementation 'androidx.recyclerview:recyclerview:1.1.0' // 追記
    implementation 'androidx.cardview:cardview:1.0.0' // 追記
}

Activity

MainActivityはFragmentを読み込むだけにしています。
FragmentをBaseFragmentという全てのFragmentのベースクラスを継承して MainActivity + 複数のFragment構成 を実現しています。
※チャット一覧画面→チャット画面のFragment切り替えで必要でした。
 暇を見つけて続きのチャット画面の記事もその内共有したいと思います。

以下の記事を参考にしています。
https://www.slideshare.net/olrandir/android-the-single-activity-multiple-fragments-pattern-one-activity-to-rule-them-all

MainActivity.java
package com.example.chat;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

import com.example.chat.fragments.BaseFragment;
import com.example.chat.fragments.ChatListFragment;

public class MainActivity extends AppCompatActivity {
    // Fragmentのベース
    private BaseFragment fragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 初回はチャット一覧のFragmentをセット
        if (fragment == null) {
            fragment = new ChatListFragment();
        }
        // main_activityにFragmentをセット
        getSupportFragmentManager().beginTransaction()
                .replace(R.id.mainContainer, fragment)
                .commit();
    }
}

Fragment

ベースとなるFragmentです。
今回は入れていませんが、共通となるボタンのイベントリスナー等をBaseFragmentに入れてしまうとコンパクトになるので便利です。

BaseFragment.java
package com.example.chat.fragments;

import android.os.Bundle;
import androidx.fragment.app.Fragment;

/**
 * ベースとなるFragment
 */
public abstract class BaseFragment extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
}

チャットリスト画面のFragmentです。

ChatListFragment.java
package com.example.chat.fragments;

import android.graphics.Color;
import android.os.Bundle;

import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.example.chat.models.DeleteChatRowData;
import com.example.chat.helpers.ChatListAdapter;
import com.example.chat.models.ChatRowData;
import com.example.chat.R;
import com.example.chat.helpers.SwipeHelper;

import java.util.ArrayList;
import java.util.List;

/**
 * チャット一覧画面用Fragment
 */
public class ChatListFragment extends BaseFragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_chat_list, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        RecyclerView rv = view.findViewById(R.id.recyclerView);

        // チャット一覧のデータList
        final List list = getChatList();
        // チャット一覧のデータListの要素数
        final int itemCount = list.size();

        // チャット一覧のアダプター
        final ChatListAdapter adapter = new ChatListAdapter(list) {
            @Override
            public void onItemClick(View view, int pos, List<ChatRowData> list) {
                // 行をクリックした時の処理を追記
            }
        };

        LinearLayoutManager llm = new LinearLayoutManager(getContext());

        rv.setHasFixedSize(true);

        rv.setLayoutManager(llm);

        rv.setAdapter(adapter);

        // スワイプを実装
        SwipeHelper swipeHelper = new SwipeHelper(getContext(), rv) {
            @Override
            public void instantiateUnderlayButton(RecyclerView.ViewHolder viewHolder, List<UnderlayButton> underlayButtons) {
                underlayButtons.add(new SwipeHelper.UnderlayButton(
                        getResources().getString(R.string.chat_list_delete_button_label),
                        0,
                        Color.parseColor(getResources().getString(R.string.chat_list_delete_button_color)),
                        new SwipeHelper.UnderlayButtonClickListener() {
                            @Override
                            public void onClick(int pos) {
                                FragmentManager fragmentManager = getFragmentManager();
                                DeleteChatRowFragment fragment = new DeleteChatRowFragment();
                                // 削除ダイアログfragmentに削除する行データをセット
                                DeleteChatRowData deleteChatRowData = new DeleteChatRowData();
                                deleteChatRowData.setList(list);
                                deleteChatRowData.setAdapter(adapter);
                                deleteChatRowData.setPosition(pos);
                                deleteChatRowData.setItemCount(itemCount);

                                // bundleを利用してデータを渡す
                                Bundle bundle = new Bundle();
                                bundle.putSerializable(getResources().getString(R.string.delete_dialog_list_tag), deleteChatRowData);
                                fragment.setArguments(bundle);

                                // ダイアログ表示
                                fragment.show(fragmentManager, "delete chat list");
                            }
                        }
                ));
            }
        };
    }

    /**
     * チャット一覧のテストデータ生成
     */
    private List<ChatRowData> getChatList() {
        List<ChatRowData> list = new ArrayList<>();

        ChatRowData data1 = new ChatRowData();
        data1.setName("田中太郎");
        data1.setText("こんにちは");
        data1.setMessageDateTime("2020/6/09 13:00");
        data1.setProfileImageId(R.drawable.sample1);
        list.add(data1);

        ChatRowData data2 = new ChatRowData();
        data2.setName("佐藤茂");
        data2.setText("おはようございます!");
        data2.setMessageDateTime("2020/6/08 8:10");
        data2.setProfileImageId(R.drawable.sample2);
        list.add(data2);

        ChatRowData data3 = new ChatRowData();
        data3.setName("taro");
        data3.setText("何時だっけ?");
        data3.setMessageDateTime("2020/6/07 20:09");
        data3.setProfileImageId(R.drawable.sample3);
        list.add(data3);

        ChatRowData data4 = new ChatRowData();
        data4.setName("hanako");
        data4.setText("教科書を貸してください");
        data4.setMessageDateTime("2020/6/06 07:00");
        data4.setProfileImageId(R.drawable.sample4);
        list.add(data4);

        ChatRowData data5 = new ChatRowData();
        data5.setName("たなか");
        data5.setText("無理");
        data5.setMessageDateTime("2020/6/06 01:05");
        data5.setProfileImageId(R.drawable.sample5);
        list.add(data5);

        ChatRowData data6 = new ChatRowData();
        data6.setName("小林");
        data6.setText("いいよ");
        data6.setMessageDateTime("2020/6/05 14:22");
        data6.setProfileImageId(R.drawable.sample6);
        list.add(data6);

        ChatRowData data7 = new ChatRowData();
        data7.setName("ペタジーニ");
        data7.setText("帰りたい");
        data7.setMessageDateTime("2020/6/05 13:00");
        data7.setProfileImageId(R.drawable.sample7);
        list.add(data7);

        ChatRowData data8 = new ChatRowData();
        data8.setName("Hayato");
        data8.setText("映画を見に行きましょう先輩!");
        data8.setMessageDateTime("2020/6/04 21:50");
        data8.setProfileImageId(R.drawable.sample8);
        list.add(data8);

        ChatRowData data9 = new ChatRowData();
        data9.setName("Tom");
        data9.setText("lol");
        data9.setMessageDateTime("2020/5/30 2:30");
        data9.setProfileImageId(R.drawable.sample9);
        list.add(data9);

        ChatRowData data10 = new ChatRowData();
        data10.setName("y.matsumoto");
        data10.setText("やったぜ");
        data10.setMessageDateTime("2020/5/29 4:00");
        data10.setProfileImageId(R.drawable.sample10);
        list.add(data10);

        return list;
    }
}

削除ダイアログのFragmentです。

DeleteChatRowFragment.java
package com.example.chat.fragments;

import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.Gravity;
import android.widget.TextView;

import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.RecyclerView;

import com.example.chat.R;
import com.example.chat.models.DeleteChatRowData;

import java.util.List;

/**
 * 削除ダイアログ用Fragment
 */
public class DeleteChatRowFragment extends DialogFragment {
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // setCancelable(false)でダイアログ外を押しても閉じない
        this.setCancelable(false);
        TextView title = new TextView(getContext());
        title.setText(getResources().getString(R.string.delete_dialog_message));
        title.setPadding(10, 50, 10, 10);
        title.setGravity(Gravity.CENTER);

        return new AlertDialog.Builder(getActivity())
                .setCustomTitle(title)
                // OKが押された場合
                .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        // 削除処理
                        Bundle bundle = getArguments();
                        DeleteChatRowData deleteChatRowData =
                                (DeleteChatRowData) bundle.getSerializable(getResources().getString(R.string.delete_dialog_list_tag));
                        List list = deleteChatRowData.getList();
                        RecyclerView.Adapter adapter = deleteChatRowData.getAdapter();
                        int pos = deleteChatRowData.getPosition();
                        int itemCount = deleteChatRowData.getItemCount();
                        // チャット一覧Listから押された行のpositionの順番の要素を削除
                        list.remove(pos);
                        // アダプターに要素を削除したことを通知
                        deleteChatRowData.getAdapter().notifyItemRemoved(pos);
                        // チャット一覧に変更があったことを通知しバインドし直す
                        adapter.notifyItemRangeChanged(pos, itemCount);
                    }
                })
                // Cancelが押された場合
                .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        // スワイプを戻す
                        Bundle bundle = getArguments();
                        DeleteChatRowData deleteChatRowData =
                                (DeleteChatRowData) bundle.getSerializable(getResources().getString(R.string.delete_dialog_list_tag));
                        RecyclerView.Adapter adapter = deleteChatRowData.getAdapter();
                        int pos = deleteChatRowData.getPosition();

                        // スワイプが元に戻る
                        adapter.notifyItemChanged(pos);
                    }
                })
                .create();
    }

    // アプリがバックグラウンドに回った時終了させない
    @Override
    public void onPause() {
        super.onPause();
        dismiss();
    }
}

ヘルパー

チャットの一覧表示で使用します。

ChatListAdapter.java
package com.example.chat.helpers;

import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

import androidx.recyclerview.widget.RecyclerView;

import com.example.chat.R;
import com.example.chat.models.ChatRowData;

import java.util.List;

/**
 * チャット一覧表示に使用するAdapterクラス
 */
public class ChatListAdapter extends RecyclerView.Adapter<ViewHolder> {
    private List<ChatRowData> list;

    public ChatListAdapter(List<ChatRowData> list) {
        this.list = list;
    }

    /**
     * チャット一覧のViewHolderを作成する
     */
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // 一行分のlayoutをViewに読み込む
        View inflate = LayoutInflater.from(parent.getContext()).inflate(R.layout.chat_list_row, parent, false);
        final ViewHolder vh = new ViewHolder(inflate);

        // クリックリスナーを登録
        inflate.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // クリックされた行のpositionを取得
                int position = vh.getAdapterPosition();
                // Viewの操作はActivityかFragmentでハンドリングしなくてはいけないので実処理は書かない
                onItemClick(v, position, list);
            }
        });

        // タッチリスナーを登録
        inflate.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                // Viewの操作はActivityかFragmentでハンドリングしなくてはいけないので実処理は書かない
                return onItemTouch(v);
            }
        });
        return vh;
    }

    /**
     * ViewHolder内のViewにチャット一覧Listのデータをbindする
     */
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        String messageDateTime = list.get(position).getMessageDateTime();
        holder.nameView.setText(list.get(position).getName());
        holder.textView.setText(list.get(position).getText());
        holder.timeView.setText(messageDateTime);
        holder.profileView.setImageResource(list.get(position).getProfileImageId());
    }

    /**
     * チャット一覧Listの要素数を設定する
     */
    @Override
    public int getItemCount() {
        return list.size();
    }

    /**
     * ChatListFragmentでoverrideして処理させる
     */
    public void onItemClick(View view, int pos, List<ChatRowData> list) {
        ;
    }

    /**
     * ChatListFragmentでoverrideして処理させる
     */
    public boolean onItemTouch(View view) {
        return false;
    }
}

ViewHolder.java
package com.example.chat.helpers;

import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.recyclerview.widget.RecyclerView;

import com.example.chat.R;

/**
 * ViewHolderクラス
 * 一行分を構成するViewを定義しておく
 */
public class ViewHolder extends RecyclerView.ViewHolder {
    public TextView nameView;
    public TextView textView;
    public TextView timeView;
    public ImageView profileView;

    public ViewHolder(View itemView) {
        super(itemView);
        nameView = itemView.findViewById(R.id.name);
        textView = itemView.findViewById(R.id.text);
        timeView = itemView.findViewById(R.id.time);
        profileView = itemView.findViewById(R.id.profileImage);
    }
}

横スワイプの動きで使用します。
こちらの記事をかなり参考にしました。

https://www.it-swarm.dev/ja/android/%E3%82%B9%E3%83%AF%E3%82%A4%E3%83%97%E3%81%AErecyclerview-itemtouchhelper%E3%83%9C%E3%82%BF%E3%83%B3/833735822/amp/

SwipeHelper.java
package com.example.chat.helpers;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;

import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;

/**
 * スワイプのヘルパークラス
 */
public abstract class SwipeHelper extends ItemTouchHelper.SimpleCallback {
    // スワイプで表示されるDELETEボタンのwidth
    public static final int BUTTON_WIDTH = 230;
    private RecyclerView recyclerView;
    private List<UnderlayButton> buttons;
    private GestureDetector gestureDetector;
    private int swipedPos = -1;
    private float swipeThreshold = 0.5f;
    private Map<Integer, List<UnderlayButton>> buttonsBuffer;
    private Queue<Integer> recoverQueue;

    private GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            for (UnderlayButton button : buttons) {
                if (button.onClick(e.getX(), e.getY()))
                    break;
            }

            return true;
        }
    };

    private View.OnTouchListener onTouchListener = new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent e) {
            if (swipedPos < 0) return false;
            Point point = new Point((int) e.getRawX(), (int) e.getRawY());

            RecyclerView.ViewHolder swipedViewHolder = recyclerView.findViewHolderForAdapterPosition(swipedPos);
            View swipedItem = swipedViewHolder.itemView;
            Rect rect = new Rect();
            swipedItem.getGlobalVisibleRect(rect);

            if (e.getAction() == MotionEvent.ACTION_DOWN
                    || e.getAction() == MotionEvent.ACTION_UP
                    || e.getAction() == MotionEvent.ACTION_MOVE) {
                if (rect.top < point.y && rect.bottom > point.y)
                    gestureDetector.onTouchEvent(e);
                else {
                    recoverQueue.add(swipedPos);
                    swipedPos = -1;
                    recoverSwipedItem();
                }
            }
            return false;
        }
    };

    public SwipeHelper(Context context, RecyclerView recyclerView) {
        super(0, ItemTouchHelper.LEFT);
        this.recyclerView = recyclerView;
        this.buttons = new ArrayList<>();
        this.gestureDetector = new GestureDetector(context, gestureListener);
        this.recyclerView.setOnTouchListener(onTouchListener);
        buttonsBuffer = new HashMap<>();
        recoverQueue = new LinkedList<Integer>() {
            @Override
            public boolean add(Integer o) {
                if (contains(o))
                    return false;
                else
                    return super.add(o);
            }
        };

        attachSwipe();
    }


    @Override
    public boolean onMove(RecyclerView recyclerView,
                          RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        return false;
    }

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        int pos = viewHolder.getAdapterPosition();

        if (swipedPos != pos)
            recoverQueue.add(swipedPos);

        swipedPos = pos;

        if (buttonsBuffer.containsKey(swipedPos))
            buttons = buttonsBuffer.get(swipedPos);
        else
            buttons.clear();

        buttonsBuffer.clear();
        swipeThreshold = 0.5f * buttons.size() * BUTTON_WIDTH;
        recoverSwipedItem();
    }

    @Override
    public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) {
        return swipeThreshold;
    }

    @Override
    public float getSwipeEscapeVelocity(float defaultValue) {
        return 0.1f * defaultValue;
    }

    @Override
    public float getSwipeVelocityThreshold(float defaultValue) {
        return 5.0f * defaultValue;
    }

    @Override
    public void onChildDraw(Canvas c, RecyclerView recyclerView,
                            RecyclerView.ViewHolder viewHolder,
                            float dX, float dY, int actionState, boolean isCurrentlyActive) {
        int pos = viewHolder.getAdapterPosition();
        float translationX = dX;
        View itemView = viewHolder.itemView;

        if (pos < 0) {
            swipedPos = pos;
            return;
        }

        if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
            if (dX < 0) {
                List<UnderlayButton> buffer = new ArrayList<>();

                if (!buttonsBuffer.containsKey(pos)) {
                    instantiateUnderlayButton(viewHolder, buffer);
                    buttonsBuffer.put(pos, buffer);
                } else {
                    buffer = buttonsBuffer.get(pos);
                }

                translationX = dX * buffer.size() * BUTTON_WIDTH / itemView.getWidth();
                drawButtons(c, itemView, buffer, pos, translationX);
            }
        }

        super.onChildDraw(c, recyclerView, viewHolder, translationX, dY, actionState, isCurrentlyActive);
    }

    private synchronized void recoverSwipedItem() {
        while (!recoverQueue.isEmpty()) {
            int pos = recoverQueue.poll();
            if (pos > -1) {
                recyclerView.getAdapter().notifyItemChanged(pos);
            }
        }
    }

    private void drawButtons(Canvas c, View itemView, List<UnderlayButton> buffer, int pos, float dX) {
        float right = itemView.getRight();
        float dButtonWidth = (-1) * dX / buffer.size();

        for (UnderlayButton button : buffer) {
            float left = right - dButtonWidth;
            button.onDraw(
                    c,
                    new RectF(
                            left,
                            itemView.getTop(),
                            right,
                            itemView.getBottom()
                    ),
                    pos
            );

            right = left;
        }
    }

    public void attachSwipe() {
        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(this);
        itemTouchHelper.attachToRecyclerView(recyclerView);
    }

    public abstract void instantiateUnderlayButton(RecyclerView.ViewHolder viewHolder, List<UnderlayButton> underlayButtons);

    public static class UnderlayButton {
        private String text;
        private int imageResId;
        private int color;
        private int pos;
        private RectF clickRegion;
        private UnderlayButtonClickListener clickListener;

        public UnderlayButton(String text, int imageResId, int color, UnderlayButtonClickListener clickListener) {
            this.text = text;
            this.imageResId = imageResId;
            this.color = color;
            this.clickListener = clickListener;
        }

        public boolean onClick(float x, float y) {
            if (clickRegion != null && clickRegion.contains(x, y)) {
                clickListener.onClick(pos);
                return true;
            }

            return false;
        }

        public void onDraw(Canvas c, RectF rect, int pos) {
            Paint p = new Paint();

            // 背景色セット
            p.setColor(color);
            c.drawRect(rect, p);

            // DELETEの文字色セット
            p.setColor(Color.WHITE);
            p.setTextSize(50);

            Rect r = new Rect();
            float cHeight = rect.height();
            float cWidth = rect.width();
            p.setTextAlign(Paint.Align.LEFT);
            p.getTextBounds(text, 0, text.length(), r);
            float x = cWidth / 2f - r.width() / 2f - r.left;
            float y = cHeight / 2f + r.height() / 2f - r.bottom;
            c.drawText(text, rect.left + x, rect.top + y, p);

            clickRegion = rect;
            this.pos = pos;
        }
    }

    public interface UnderlayButtonClickListener {
        void onClick(int pos);
    }
}

モデル

チャット一覧の一行分のデータを詰めます。

ChatRowData.java
package com.example.chat.models;

/**
 * 一行分のデータモデルクラス
 */
public class ChatRowData {
    private String name;
    private String text;
    private String messageDateTime;
    private int profileImageId;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public String getMessageDateTime() {
        return messageDateTime;
    }

    public void setMessageDateTime(String messageDateTime) {
        this.messageDateTime = messageDateTime;
    }

    public int getProfileImageId() {
        return profileImageId;
    }

    public void setProfileImageId(int profileImageId) {
        this.profileImageId = profileImageId;
    }
}

削除ダイアログ用に必要なデータを詰めます。

ChatRowData.java
package com.example.chat.models;

/**
 * 一行分のデータモデルクラス
 */
public class ChatRowData {
    private String name;
    private String text;
    private String messageDateTime;
    private int profileImageId;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public String getMessageDateTime() {
        return messageDateTime;
    }

    public void setMessageDateTime(String messageDateTime) {
        this.messageDateTime = messageDateTime;
    }

    public int getProfileImageId() {
        return profileImageId;
    }

    public void setProfileImageId(int profileImageId) {
        this.profileImageId = profileImageId;
    }
}

レイアウト

activity_mainにはFragmentを入れるためのFrameLayout以外は記述しません。
本当はヘッダーとフッターも共通パーツ化してしまえばメンテナンス性も向上するのですが、

・追加予定のチャット画面への遷移アニメーション(右から画面全体にチャット画面のレイヤーが覆い被さる感じ)が中々困難になる点
・バックキーの挙動を組み合わせると地獄に陥った点

から共通パーツ化は見送りました。。
次回の記事にて有識者の方の改善策がもし聞けたら本当に嬉しいです。。

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"
    android:focusableInTouchMode="true"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/mainContainer"
        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">
    </FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

チャット一覧画面用のfragment layoutです。

fragment_chat_list.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"
    android:background="@color/background_light_gray"
    tools:context=".fragments.ChatListFragment">

    <View
        android:id="@+id/headerView"
        android:layout_width="wrap_content"
        android:layout_height="50dp"
        android:background="@color/background_dark_gray"
        android:contextClickable="false"
        android:layerType="none"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/chatListSubject"
        android:layout_width="63dp"
        android:layout_height="19dp"
        android:text="@string/chat_list_subject_label"
        android:textAlignment="center"
        android:textAppearance="@style/TextAppearance.AppCompat.Medium"
        android:textColor="@color/font_color_black"
        app:layout_constraintBottom_toBottomOf="@+id/headerView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/headerView"
        app:layout_constraintVertical_bias="0.48" />

    <View
        android:id="@+id/headerBorder"
        android:layout_width="match_parent"
        android:layout_height="0.5dp"
        android:layout_marginTop="48dp"
        android:background="@color/background_border"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/headerView" />

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:isScrollContainer="false"
        app:layout_constraintBottom_toTopOf="@+id/footerBorder"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/headerView"
        app:layout_constraintVertical_bias="0.0">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
    </LinearLayout>

    <View
        android:id="@+id/background"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:background="@color/background_dark_gray"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/footerBorder" />

    <ImageButton
        android:id="@+id/homeButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="80dp"
        android:background="@color/background_transparent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/footerBorder"
        app:srcCompat="@drawable/home" />

    <ImageButton
        android:id="@+id/chatListButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/background_transparent"
        app:layout_constraintEnd_toStartOf="@+id/userButton"
        app:layout_constraintStart_toEndOf="@+id/homeButton"
        app:layout_constraintTop_toTopOf="@+id/homeButton"
        app:srcCompat="@drawable/fukidashi" />

    <ImageButton
        android:id="@+id/userButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="80dp"
        android:background="@color/background_transparent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="@+id/chatListButton"
        app:srcCompat="@drawable/person" />

    <View
        android:id="@+id/footerBorder"
        android:layout_width="match_parent"
        android:layout_height="0.5dp"
        android:layout_marginBottom="72dp"
        android:background="@color/background_border"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

チャット一覧一行分のlayoutです。

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

    <androidx.cardview.widget.CardView
        android:id="@+id/chatListCardView"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        app:cardCornerRadius="15dp"
        app:cardElevation="0dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <ImageView
            android:id="@+id/profileImage"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@mipmap/ic_launcher" />
    </androidx.cardview.widget.CardView>

    <TextView
        android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:layout_marginStart="16dp"
        android:text="name"
        android:textColor="@color/font_color_black"
        android:textSize="12sp"
        android:textStyle="bold"
        app:layout_constraintStart_toEndOf="@+id/chatListCardView"
        app:layout_constraintTop_toTopOf="@+id/chatListCardView" />

    <TextView
        android:id="@+id/time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:text="time"
        android:textColor="@color/font_color_black"
        android:textSize="8sp"
        app:layout_constraintBottom_toBottomOf="@+id/name"
        app:layout_constraintStart_toEndOf="@+id/name" />

    <TextView
        android:id="@+id/text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="8dp"
        android:text="text"
        android:textColor="@color/font_color_black"
        android:textSize="12sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/name"
        app:layout_constraintTop_toBottomOf="@+id/name" />

    <View
        android:id="@+id/line"
        android:layout_width="match_parent"
        android:layout_height="0.3dp"
        android:layout_marginTop="8dp"
        android:background="@color/background_border"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/chatListCardView" />

</androidx.constraintlayout.widget.ConstraintLayout>

values

使用する定数ファイルです

colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- フォントの色 黒 -->
    <color name="font_color_black">#5E5E5E</color>
    <!-- フォント オレンジ -->
    <color name="font_color_orange">#FF9900</color>
    <!-- background body -->
    <color name="background_light_gray">#EBEBEB</color>
    <!-- background header, footer -->
    <color name="background_dark_gray">#E5E5E5</color>
    <!-- background 区切り線 -->
    <color name="background_border">#838383</color>
    <!-- background 透過 -->
    <color name="background_transparent">#00E5E5E5</color>
</resources>

strings.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 定数管理ファイル -->
<resources>
    <!-- アプリ名 -->
    <string name="app_name">Chat</string>
    <!-- チャット一覧画面 -->
    <string name="chat_list_subject_label">CHATS</string>
    <string name="chat_list_delete_button_color">#FF9900</string>
    <string name="chat_list_delete_button_label">DELETE</string>
    <string name="delete_dialog_message">このチャット履歴を削除しても\nよろしいでしょうか?</string>
    <string name="delete_dialog_list_tag">chatList</string>
</resources>


styles.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorAccent">@color/font_color_orange</item>
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
    </style>
</resources>


以上のコードで冒頭のサンプルの動きが再現できると思います。
こちらにソースも上がっていますので宜しければご確認ください。
https://github.com/yuta-matsumoto/chat

最後に

ハンズオン系の記事を初めて書きましたが、どこまで説明すべきか非常に戸惑いますね。。
もしどなたかの助けになれましたらとても嬉しいです!

yuta-matsumoto
どうも、初めまして。 インターノウス株式会社という会社で自社開発に携わっている者です。 今までGoogle先生に助けてもらってきたばかりなので、少しばかり役に立ちたいと思っています。 よろしくお願いします。 何かありましたらこちらにご一報ください。 y.matsumoto@internous.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした