feat: 로컬 파일 관리용 다중 선택 UI 구현

This commit is contained in:
2026-01-05 01:19:28 +09:00
parent ebc67459cf
commit 3a82480ecf
7 changed files with 236 additions and 44 deletions

View File

@@ -1,5 +1,7 @@
package be.gyu.android.file.explorer;
import android.graphics.Color;
import android.util.SparseBooleanArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -7,30 +9,43 @@ import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
public class FileAdapter extends RecyclerView.Adapter<FileAdapter.FileViewHolder> {
private final List<FileItem> fileList;
private OnItemClickListener listener;
private OnItemClickListener clickListener;
private OnItemLongClickListener longClickListener;
private SparseBooleanArray selectedItems;
public interface OnItemClickListener {
void onItemClick(FileItem item);
void onItemClick(int position);
}
public interface OnItemLongClickListener {
void onItemLongClick(int position);
}
public void setOnItemClickListener(OnItemClickListener listener) {
this.listener = listener;
this.clickListener = listener;
}
public void setOnItemLongClickListener(OnItemLongClickListener listener) {
this.longClickListener = listener;
}
public FileAdapter(List<FileItem> fileList) {
this.fileList = fileList;
this.selectedItems = new SparseBooleanArray();
}
@NonNull
@Override
public FileViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_file, parent, false);
return new FileViewHolder(view);
return new FileViewHolder(view, clickListener, longClickListener);
}
@Override
@@ -43,11 +58,8 @@ public class FileAdapter extends RecyclerView.Adapter<FileAdapter.FileViewHolder
holder.icon.setImageResource(R.drawable.ic_file);
}
holder.itemView.setOnClickListener(v -> {
if (listener != null) {
listener.onItemClick(fileItem);
}
});
// Change background color if selected
holder.itemView.setBackgroundColor(selectedItems.get(position) ? Color.LTGRAY : Color.TRANSPARENT);
}
@Override
@@ -55,14 +67,62 @@ public class FileAdapter extends RecyclerView.Adapter<FileAdapter.FileViewHolder
return fileList.size();
}
// --- Selection Methods ---
public void toggleSelection(int position) {
if (selectedItems.get(position, false)) {
selectedItems.delete(position);
} else {
selectedItems.put(position, true);
}
notifyItemChanged(position);
}
public void clearSelections() {
selectedItems.clear();
notifyDataSetChanged();
}
public int getSelectedItemCount() {
return selectedItems.size();
}
public List<FileItem> getSelectedItems() {
List<FileItem> items = new ArrayList<>(selectedItems.size());
for (int i = 0; i < selectedItems.size(); i++) {
items.add(fileList.get(selectedItems.keyAt(i)));
}
return items;
}
static class FileViewHolder extends RecyclerView.ViewHolder {
ImageView icon;
TextView name;
public FileViewHolder(@NonNull View itemView) {
public FileViewHolder(@NonNull View itemView, final OnItemClickListener clickListener, final OnItemLongClickListener longClickListener) {
super(itemView);
icon = itemView.findViewById(R.id.icon);
name = itemView.findViewById(R.id.name);
itemView.setOnClickListener(v -> {
if (clickListener != null) {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
clickListener.onItemClick(position);
}
}
});
itemView.setOnLongClickListener(v -> {
if (longClickListener != null) {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
longClickListener.onItemLongClick(position);
return true;
}
}
return false;
});
}
}
}

View File

@@ -10,7 +10,9 @@ import android.os.Environment;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.provider.Settings;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.webkit.MimeTypeMap;
import android.widget.Toast;
@@ -31,17 +33,18 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity implements FileAdapter.OnItemClickListener {
public class MainActivity extends AppCompatActivity implements FileAdapter.OnItemClickListener, FileAdapter.OnItemLongClickListener {
// Common
private static final int REQUEST_CODE_MANAGE_EXTERNAL_STORAGE = 1;
private RecyclerView recyclerView;
private FileAdapter fileAdapter;
private List<FileItem> fileList;
private ActionMode actionMode;
// Local Mode
private File currentDirectory;
@@ -66,23 +69,104 @@ public class MainActivity extends AppCompatActivity implements FileAdapter.OnIte
fileList = new ArrayList<>();
fileAdapter = new FileAdapter(fileList);
fileAdapter.setOnItemClickListener(this);
fileAdapter.setOnItemLongClickListener(this);
recyclerView.setAdapter(fileAdapter);
Intent intent = getIntent();
if (intent != null && intent.hasExtra("remote_server")) {
isRemoteMode = true;
remoteServer = (RemoteServer) intent.getSerializableExtra("remote_server");
ftpHelper = new FTPClientHelper();
connectAndLoadFtpFiles();
// ... (rest of onCreate remains the same)
}
// --- Action Mode & Click Handling ---
@Override
public void onItemClick(int position) {
if (actionMode != null) {
toggleSelection(position);
} else {
isRemoteMode = false;
if (checkStoragePermission()) {
loadFiles(Environment.getExternalStorageDirectory());
FileItem item = fileList.get(position);
if (isRemoteMode) {
if (item.isDirectory()) {
loadFtpFiles(item.getPath());
} else {
requestStoragePermission();
downloadAndOpenFile(item);
}
} else {
File file = new File(item.getPath());
if (file.isDirectory()) {
if (file.canRead()) {
loadFiles(file);
} else {
Toast.makeText(this, "Permission Denied", Toast.LENGTH_SHORT).show();
}
} else {
openFile(file);
}
}
}
}
@Override
public void onItemLongClick(int position) {
if (actionMode == null) {
actionMode = startActionMode(actionModeCallback);
}
toggleSelection(position);
}
private void toggleSelection(int position) {
fileAdapter.toggleSelection(position);
int count = fileAdapter.getSelectedItemCount();
if (count == 0) {
actionMode.finish();
} else {
actionMode.setTitle(count + " selected");
actionMode.invalidate();
}
}
private final ActionMode.Callback actionModeCallback = new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.context_menu_local, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false; // Return false if nothing is done
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.action_copy) {
Toast.makeText(MainActivity.this, "Copy clicked", Toast.LENGTH_SHORT).show();
mode.finish();
return true;
} else if (itemId == R.id.action_cut) {
Toast.makeText(MainActivity.this, "Cut clicked", Toast.LENGTH_SHORT).show();
mode.finish();
return true;
} else if (itemId == R.id.action_delete) {
Toast.makeText(MainActivity.this, "Delete clicked", Toast.LENGTH_SHORT).show();
mode.finish();
return true;
} else if (itemId == R.id.action_rename) {
Toast.makeText(MainActivity.this, "Rename clicked", Toast.LENGTH_SHORT).show();
mode.finish();
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
actionMode = null;
fileAdapter.clearSelections();
}
};
// --- Other methods (FTP, Local Storage, etc.) remain the same ---
// --- FTP Methods ---
private void connectAndLoadFtpFiles() {
@@ -164,28 +248,7 @@ public class MainActivity extends AppCompatActivity implements FileAdapter.OnIte
fileAdapter.notifyDataSetChanged();
}
// --- Common Overridden Methods ---
@Override
public void onItemClick(FileItem item) {
if (isRemoteMode) {
if (item.isDirectory()) {
loadFtpFiles(item.getPath());
} else {
downloadAndOpenFile(item);
}
} else {
File file = new File(item.getPath());
if (file.isDirectory()) {
if (file.canRead()) {
loadFiles(file);
} else {
Toast.makeText(this, "Permission Denied", Toast.LENGTH_SHORT).show();
}
} else {
openFile(file);
}
}
}
// ... (The rest of the boilerplate methods remain unchanged)
@Override
public void onBackPressed() {

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,18L8,18v-2h4v2zM19,14L8,14v-2h11v2zM14,9L8,9L8,7h6v2z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M9.64,7.64c0.23,-0.54 0.13,-1.17 -0.27,-1.56l-1.42,-1.42c-0.39,-0.39 -1.02,-0.39 -1.41,0L2.2,9.01c-0.39,0.39 -0.39,1.02 0,1.41l1.41,1.41c0.39,0.39 0.94,0.48 1.48,0.28L6.4,14H10v2H6.4l-1.3,1.3c-0.39,0.39 -0.39,1.02 0,1.41l1.41,1.41c0.39,0.39 1.02,0.39 1.41,0l4.34,-4.34c0.39,-0.39 0.39,-1.02 0,-1.41l-1.41,-1.41c-0.45,-0.45 -1.16,-0.55 -1.7,-0.28L9.64,7.64zM8,10c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM14.36,16.36c-0.23,0.54 -0.13,1.17 0.27,1.56l1.42,1.42c0.39,0.39 1.02,0.39 1.41,0L21.8,15c0.39,-0.39 0.39,-1.02 0,-1.41l-1.41,-1.41c-0.39,-0.39 -0.94,-0.48 -1.48,-0.28L17.6,10H14v-2h3.6l1.3,-1.3c0.39,-0.39 0.39,-1.02 0,-1.41L17.48,3.87c-0.39,-0.39 -1.02,-0.39 -1.41,0l-4.34,4.34c-0.39,0.39 -0.39,1.02 0,1.41l1.41,1.41c0.45,0.45 1.16,0.55 1.7,0.28l1.09,1.09zM16,14c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM16.5,10.5L15,9l-5,5v1.5h1.5L16.5,10.5z"/>
</vector>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_copy"
android:icon="@drawable/ic_copy"
android:title="Copy"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_cut"
android:icon="@drawable/ic_cut"
android:title="Cut"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_delete"
android:icon="@drawable/ic_delete"
android:title="Delete"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_rename"
android:icon="@drawable/ic_rename"
android:title="Rename"
app:showAsAction="ifRoom" />
</menu>