環境
Android9.0 Pie Java
はじめに
最近Android Javaでチャットアプリを作成していまして、LINE風のチャット一覧削除を実装しました。
RecyclerViewのリスト表示と削除機能の記事は結構ありますが、
横スワイプ→ダイアログ表示→削除 MainActivity + 複数のFragment構成
の実装例は日本語の記事では見つからなかったので共有させて頂きます。
(ベストプラクティスかは怪しいので参考程度にお願いします。。ご指摘大歓迎です!)
完成品URL
非常にコンテンツが長いので、完成品を取り合えず手に入れたい方はこちらからどうぞ。
https://github.com/yuta-matsumoto/chat/tree/d314d45e450882404c1920f2f155a18a3eca1fb8
※【Android9.0 Pie Java】LINE風にチャット一覧画面→個人チャット画面の遷移アニメーションを実装(サンプルアプリ付)
という記事でバージョンアップしました!少し古いコードとなりますが、動作確認済みです。
ディレクトリ構成
ビルドファイルやマニフェストファイルは省略します
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
コード
依存関係
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
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に入れてしまうとコンパクトになるので便利です。
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です。
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です。
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();
}
}
ヘルパー
チャットの一覧表示で使用します。
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;
}
}
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);
}
}
横スワイプの動きで使用します。
こちらの記事をかなり参考にしました。
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);
}
}
モデル
チャット一覧の一行分のデータを詰めます。
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;
}
}
削除ダイアログ用に必要なデータを詰めます。
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以外は記述しません。
本当はヘッダーとフッターも共通パーツ化してしまえばメンテナンス性も向上するのですが、
・追加予定のチャット画面への遷移アニメーション(右から画面全体にチャット画面のレイヤーが覆い被さる感じ)が中々困難になる点
・バックキーの挙動を組み合わせると地獄に陥った点
から共通パーツ化は見送りました。。
次回の記事にて有識者の方の改善策がもし聞けたら本当に嬉しいです。。
<?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です。
<?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です。
<?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
使用する定数ファイルです
<?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>
<?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>
<?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
最後に
ハンズオン系の記事を初めて書きましたが、どこまで説明すべきか非常に戸惑いますね。。
もしどなたかの助けになれましたらとても嬉しいです!