[Java] [Android 9.0 Pie Java] Implement horizontal swipe → dialog display → delete in chat application

11 minute read

Environment

Android9.0 Pie Java

Introduction

Recently, I was creating a chat application on Android Java, and implemented a LINE style chat list deletion. There are quite a few articles on the list display and deletion functions of RecyclerView,

Side swipe → Dialog display → Delete MainActivity + multiple Fragment configurations

I couldn’t find the implementation example of in the Japanese article, so I’ll share it. (It’s suspicious about the best practice, so please use it as a reference. We welcome your suggestions!)

You can implement this 243e5a5ccb079ad0d385004d69a930ba.gif

Finished product URL

The content is very long, so if you want to get the finished product without getting it, please click here. https://github.com/yuta-matsumoto/chat

Directory structure

Omit build files and manifest files

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 (hereinafter omitted) │ ├ layout/ │ │ │ ├ activity_main.xml │ │ │ ├ chat_list_row.xml │ │ │ └fragment_chat_list.xml │ │ └ values/ │ │ ├ colors.xml │ │ ├ strings.xml │ │ └ styles.xml

code

Dependency

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' // add
    implementation'androidx.cardview:cardview:1.0.0' // Add
}

Activity

MainActivity just reads the Fragment. Fragment inherits all the base classes of Fragment called BaseFragment to realize MainActivity + multiple Fragment configurations. *It was necessary to switch the chat list screen to the chat screen Fragment. I would like to find some free time and share the following chat screen articles.

Refer to the following article. 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 base
    private BaseFragment fragment;

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

        // First set the chat list Fragment
        if (fragment == null) {
            fragment = new ChatListFragment();
        }
        // Set Fragment in main_activity
        getSupportFragmentManager().beginTransaction()
                .replace(R.id.mainContainer, fragment)
                .commit();
    }
}

Fragment

The base Fragment. I haven’t put it in this time, but it’s convenient to put common button event listeners in the BaseFragment as it will be compact.

BaseFragment.java


package com.example.chat.fragments;

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

/**
 * Base Fragment
 */
public abstract class BaseFragment extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
}

Fragment of chat list screen.

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 for chat list screen
 */
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 of chat data
        final List list = getChatList();
        // number of data list elements in chat list
        final int itemCount = list.size();

        // Chat list adapter
        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());
    }

    /**
     * Set the number of elements in the chat list List
     */
    @Override
    public int getItemCount() {
        return list.size();
    }

    /**
     * Override with ChatListFragment and process
     */
    public void onItemClick(View view, int pos, List<ChatRowData> list) {
        ;
    }

    /**
     * Override with ChatListFragment and process
     */
    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 class
 * Define the view that constitutes one line
 */
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);
    }
}

Used for horizontal swipe movement. I used this article as a reference.

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;

/**
 * Swipe helper class
 */
public abstract class SwipeHelper extends ItemTouchHelper.SimpleCallback {
    // width of DELETE button displayed by swipe
    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;
    }

    @Overridepublic 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

最後に

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