forked from upstream-mirrors/OpenTracks
Add recycler view adapter for marker list
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
package de.dennisguse.opentracks.ui.markers;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.media.ThumbnailUtils;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.util.LruCache;
|
||||
import android.util.SparseBooleanArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.databinding.MarkerListItemBinding;
|
||||
import de.dennisguse.opentracks.ui.util.ActivityUtils;
|
||||
import de.dennisguse.opentracks.ui.util.ExecutorListViewService;
|
||||
import de.dennisguse.opentracks.ui.util.ListItemUtils;
|
||||
import de.dennisguse.opentracks.ui.util.ThemeUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
public class MarkerListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements ActionMode.Callback {
|
||||
|
||||
private static final String TAG = MarkerListAdapter.class.getSimpleName();
|
||||
|
||||
private final AppCompatActivity context;
|
||||
private final RecyclerView recyclerView;
|
||||
private List<Marker> markers;
|
||||
private final SparseBooleanArray selection;
|
||||
private boolean selectionMode;
|
||||
private ActivityUtils.ContextualActionModeCallback actionModeCallback;
|
||||
private ActionMode actionMode;
|
||||
|
||||
private final ExecutorListViewService<Marker.Id> executorService = new ExecutorListViewService<>(128);
|
||||
|
||||
private final LruCache<Marker.Id, Bitmap> memoryCache;
|
||||
|
||||
public MarkerListAdapter(AppCompatActivity context, RecyclerView recyclerView, List<Marker> markers) {
|
||||
this.context = context;
|
||||
this.recyclerView = recyclerView;
|
||||
this.markers = markers;
|
||||
|
||||
selection = new SparseBooleanArray();
|
||||
selectionMode = false;
|
||||
|
||||
// Get max available VM memory, exceeding this amount will throw an OutOfMemory exception.
|
||||
final long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
|
||||
final int cacheSize = (int) (Runtime.getRuntime().maxMemory() - usedMemory) / 8;
|
||||
|
||||
memoryCache = new LruCache<>(cacheSize) {
|
||||
@Override
|
||||
protected int sizeOf(Marker.Id key, Bitmap value) {
|
||||
return value.getByteCount();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void setActionModeCallback(ActivityUtils.ContextualActionModeCallback actionModeCallback) {
|
||||
this.actionModeCallback = actionModeCallback;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.marker_list_item, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
ViewHolder viewHolder = (ViewHolder) holder;
|
||||
|
||||
viewHolder.bind(markers.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
if (markers == null) {
|
||||
return 0;
|
||||
}
|
||||
return markers.size();
|
||||
}
|
||||
|
||||
public void swapData(List<Marker> markers) {
|
||||
this.markers = markers;
|
||||
this.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
|
||||
actionMode.getMenuInflater().inflate(R.menu.list_context_menu, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
|
||||
selectionMode = true;
|
||||
actionModeCallback.onPrepare(menu, getCheckedPositions(), getCheckedIds(), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
|
||||
if (actionModeCallback.onClick(menuItem.getItemId(), getCheckedPositions(), getCheckedIds())) {
|
||||
actionMode.finish();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode actionMode) {
|
||||
selectionMode = false;
|
||||
|
||||
setAllSelected(false);
|
||||
selection.clear();
|
||||
|
||||
actionModeCallback.onDestroy();
|
||||
}
|
||||
|
||||
public void setAllSelected(boolean isSelected) {
|
||||
for (int i = 0; i < getItemCount(); i++) {
|
||||
ViewHolder holder = (ViewHolder) recyclerView.findViewHolderForLayoutPosition(i);
|
||||
holder.setSelected(isSelected);
|
||||
}
|
||||
}
|
||||
|
||||
private int[] getCheckedPositions() {
|
||||
List<Integer> positions = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < selection.size(); i++) {
|
||||
if (selection.valueAt(i)) {
|
||||
positions.add(selection.keyAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
return positions.stream().mapToInt(i -> i).toArray();
|
||||
}
|
||||
|
||||
private long[] getCheckedIds() {
|
||||
List<Long> ids = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < selection.size(); i++) {
|
||||
if (selection.valueAt(i)) {
|
||||
ViewHolder holder = (ViewHolder) recyclerView.findViewHolderForLayoutPosition(selection.keyAt(i));
|
||||
ids.add(holder.getId());
|
||||
}
|
||||
}
|
||||
|
||||
return ids.stream().mapToLong(i -> i).toArray();
|
||||
}
|
||||
|
||||
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
|
||||
|
||||
private final MarkerListItemBinding viewBinding;
|
||||
private final View view;
|
||||
|
||||
private Marker marker;
|
||||
|
||||
public ViewHolder(@NonNull View view) {
|
||||
super(view);
|
||||
|
||||
viewBinding = MarkerListItemBinding.bind(view);
|
||||
this.view = view;
|
||||
|
||||
view.setOnClickListener(this);
|
||||
view.setOnLongClickListener(this);
|
||||
}
|
||||
|
||||
public void bind(Marker marker) {
|
||||
this.marker = marker;
|
||||
|
||||
viewBinding.markerListItemPhoto.setVisibility(marker.hasPhoto() ? View.VISIBLE : View.GONE);
|
||||
viewBinding.markerListItemTextGradient.setVisibility(marker.hasPhoto() ? View.VISIBLE : View.GONE);
|
||||
if (marker.hasPhoto()) {
|
||||
int height = ThemeUtils.getPhotoHeight(context);
|
||||
ViewGroup.LayoutParams params = viewBinding.markerListItemPhoto.getLayoutParams();
|
||||
params.height = height;
|
||||
viewBinding.markerListItemPhoto.setLayoutParams(params);
|
||||
viewBinding.markerListItemTextGradient.setLayoutParams(params);
|
||||
|
||||
asyncLoadPhoto(viewBinding.markerListItemPhoto, marker.getPhotoURI(), marker.getId());
|
||||
}
|
||||
|
||||
viewBinding.markerListItemName.setText(marker.getName());
|
||||
|
||||
{
|
||||
ZoneOffset timeZone = new ContentProviderUtils(context).getTrack(marker.getTrackId()).getZoneOffset();
|
||||
ListItemUtils.setDateAndTime(context, viewBinding.markerListItemDate, viewBinding.markerListItemTime, marker.getTime(), timeZone);
|
||||
}
|
||||
|
||||
String categoryDescription = StringUtils.getCategoryDescription(marker.getCategory(), marker.getDescription());
|
||||
viewBinding.markerListItemTimeDistance.setText(categoryDescription);
|
||||
|
||||
setSelected(selection.get(getLayoutPosition()));
|
||||
}
|
||||
|
||||
public void setSelected(boolean isSelected) {
|
||||
selection.put(getLayoutPosition(), isSelected);
|
||||
view.setActivated(isSelected);
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return marker.getId().id();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (selectionMode) {
|
||||
setSelected(!view.isActivated());
|
||||
actionMode.invalidate();
|
||||
} else {
|
||||
Intent intent = IntentUtils.newIntent(context, MarkerDetailActivity.class)
|
||||
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, marker.getId());
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
actionMode = context.startSupportActionMode(MarkerListAdapter.this);
|
||||
setSelected(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It loads the photoUrl in the imageView from view.
|
||||
* It takes the photo from cache or from storage if isn't in the cache.
|
||||
*
|
||||
* @param imageView view object where photo will be loaded.
|
||||
* @param photoUri photo's url.
|
||||
* @param id marker's id where photo belong.
|
||||
*/
|
||||
private void asyncLoadPhoto(ImageView imageView, Uri photoUri, Marker.Id id) {
|
||||
|
||||
Bitmap bm = memoryCache.get(id);
|
||||
imageView.setImageBitmap(bm);
|
||||
if (bm != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
executorService.execute(id, () -> {
|
||||
try (InputStream inputStream = context.getContentResolver().openInputStream(photoUri)) {
|
||||
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
|
||||
int height = ThemeUtils.getPhotoHeight(context);
|
||||
Log.d(TAG, "Width : " + (bitmap.getWidth() / (bitmap.getHeight() / height)) + " | Height: " + height);
|
||||
final Bitmap finalPhoto = ThumbnailUtils.extractThumbnail(bitmap, bitmap.getWidth() / (bitmap.getHeight() / height), height);
|
||||
context.runOnUiThread(() -> imageView.setImageBitmap(finalPhoto));
|
||||
memoryCache.put(id, finalPhoto);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to image " + e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,9 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
@@ -180,4 +182,17 @@ public class ListItemUtils {
|
||||
textView.setShadowLayer(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setDateAndTime(Context context, TextView dateView, TextView timeView, Instant time, ZoneOffset timeZone) {
|
||||
OffsetDateTime offsetDateTime = OffsetDateTime.ofInstant(time, timeZone);
|
||||
String dateContent = StringUtils.formatDateTodayRelative(context, offsetDateTime);
|
||||
String pattern = "HH:mm";
|
||||
if (!offsetDateTime.getOffset().equals(OffsetDateTime.now().getOffset())) {
|
||||
pattern = "HH:mm x";
|
||||
}
|
||||
String timeContent = offsetDateTime.format(DateTimeFormatter.ofPattern(pattern));
|
||||
|
||||
dateView.setText(dateContent);
|
||||
timeView.setText(timeContent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,4 +52,15 @@ public class ThemeUtils {
|
||||
typedArray.recycle();
|
||||
return fontSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the photo height.
|
||||
*/
|
||||
public static int getPhotoHeight(Context context) {
|
||||
int[] attrs = new int[]{android.R.attr.listPreferredItemHeight};
|
||||
TypedArray typeArray = context.obtainStyledAttributes(attrs);
|
||||
int height = typeArray.getDimensionPixelSize(0, 128);
|
||||
typeArray.recycle();
|
||||
return 2 * height;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?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="wrap_content"
|
||||
android:background="@drawable/activated_background"
|
||||
android:padding="8dp">
|
||||
|
||||
<!-- Photo -->
|
||||
<ImageView
|
||||
android:id="@+id/marker_list_item_photo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/image_photo_marker"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toTopOf="@id/marker_list_item_bottom_line"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<!-- A gradient on top of the text -->
|
||||
<ImageView
|
||||
android:id="@+id/marker_list_item_text_gradient"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha=".6"
|
||||
android:contentDescription="@string/image_photo_marker"
|
||||
android:src="@drawable/text_gradient"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/marker_list_item_bottom_line"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<!-- Name -->
|
||||
<TextView
|
||||
android:id="@+id/marker_list_item_name"
|
||||
style="?attr/textAppearanceBody1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Main text" />
|
||||
|
||||
<!-- Date -->
|
||||
<TextView
|
||||
android:id="@+id/marker_list_item_date"
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="2022-02-17" />
|
||||
|
||||
<!-- Total time and distance -->
|
||||
<TextView
|
||||
android:id="@+id/marker_list_item_time_distance"
|
||||
style='?attr/textAppearanceBodySmall'
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="@+id/marker_list_item_name"
|
||||
app:layout_constraintTop_toBottomOf="@+id/marker_list_item_name"
|
||||
tools:text="50:43 (50.00km)" />
|
||||
|
||||
<!-- Time -->
|
||||
<TextView
|
||||
android:id="@+id/marker_list_item_time"
|
||||
style="?attr/textAppearanceBodySmall"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/marker_list_item_name"
|
||||
tools:text="10:10:10" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/marker_list_item_barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="marker_list_item_photo,marker_list_item_text_gradient,marker_list_item_name,marker_list_item_date,marker_list_item_time_distance,marker_list_item_time" />
|
||||
|
||||
<!-- Bottom Line -->
|
||||
<View
|
||||
android:id="@+id/marker_list_item_bottom_line"
|
||||
style="@style/HorizontalLine"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/marker_list_item_barrier" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
Reference in New Issue
Block a user