forked from upstream-mirrors/OpenTracks
Merge pull request #1672 from lmarz/marker
MarkerListActivity: Switch to RecyclerView
This commit is contained in:
@@ -548,6 +548,35 @@ public class ContentProviderUtils {
|
|||||||
return contentResolver.query(MarkerColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
|
return contentResolver.query(MarkerColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Marker> searchMarkers(Track.Id trackId, String query) {
|
||||||
|
String selection = null;
|
||||||
|
String[] selectionArgs = null;
|
||||||
|
String sortOrder = null;
|
||||||
|
|
||||||
|
if (query == null) {
|
||||||
|
if (trackId != null) {
|
||||||
|
selection = MarkerColumns.TRACKID + " = ?";
|
||||||
|
selectionArgs = new String[]{Long.toString(trackId.id())};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selection = MarkerColumns.NAME + " LIKE ? OR " +
|
||||||
|
MarkerColumns.DESCRIPTION + " LIKE ? OR " +
|
||||||
|
MarkerColumns.CATEGORY + " LIKE ?";
|
||||||
|
selectionArgs = new String[]{"%" + query + "%", "%" + query + "%", "%" + query + "%"};
|
||||||
|
sortOrder = MarkerColumns.DEFAULT_SORT_ORDER + " DESC";
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<Marker> markers = new ArrayList<>();
|
||||||
|
try (Cursor cursor = getMarkerCursor(null, selection, selectionArgs, sortOrder, -1)) {
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
do {
|
||||||
|
markers.add(createMarker(cursor));
|
||||||
|
} while (cursor.moveToNext());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fills a {@link TrackPoint} from a cursor.
|
* Fills a {@link TrackPoint} from a cursor.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -18,19 +18,16 @@ package de.dennisguse.opentracks.ui.markers;
|
|||||||
|
|
||||||
import android.app.SearchManager;
|
import android.app.SearchManager;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.widget.SearchView;
|
import androidx.appcompat.widget.SearchView;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.loader.content.CursorLoader;
|
|
||||||
import androidx.loader.content.Loader;
|
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.AbstractActivity;
|
import de.dennisguse.opentracks.AbstractActivity;
|
||||||
@@ -38,15 +35,12 @@ import de.dennisguse.opentracks.R;
|
|||||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||||
import de.dennisguse.opentracks.data.models.Marker;
|
import de.dennisguse.opentracks.data.models.Marker;
|
||||||
import de.dennisguse.opentracks.data.models.Track;
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
import de.dennisguse.opentracks.data.tables.MarkerColumns;
|
|
||||||
import de.dennisguse.opentracks.databinding.MarkerListBinding;
|
import de.dennisguse.opentracks.databinding.MarkerListBinding;
|
||||||
import de.dennisguse.opentracks.services.RecordingStatus;
|
import de.dennisguse.opentracks.services.RecordingStatus;
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||||
import de.dennisguse.opentracks.share.ShareUtils;
|
import de.dennisguse.opentracks.share.ShareUtils;
|
||||||
import de.dennisguse.opentracks.ui.markers.DeleteMarkerDialogFragment.DeleteMarkerCaller;
|
|
||||||
import de.dennisguse.opentracks.ui.util.ActivityUtils;
|
import de.dennisguse.opentracks.ui.util.ActivityUtils;
|
||||||
import de.dennisguse.opentracks.ui.util.ScrollVisibleViews;
|
|
||||||
import de.dennisguse.opentracks.util.IntentUtils;
|
import de.dennisguse.opentracks.util.IntentUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,23 +48,20 @@ import de.dennisguse.opentracks.util.IntentUtils;
|
|||||||
*
|
*
|
||||||
* @author Leif Hendrik Wilden
|
* @author Leif Hendrik Wilden
|
||||||
*/
|
*/
|
||||||
public class MarkerListActivity extends AbstractActivity implements DeleteMarkerCaller {
|
public class MarkerListActivity extends AbstractActivity {
|
||||||
|
|
||||||
public static final String EXTRA_TRACK_ID = "track_id";
|
public static final String EXTRA_TRACK_ID = "track_id";
|
||||||
|
|
||||||
private static final String TAG = MarkerListActivity.class.getSimpleName();
|
|
||||||
|
|
||||||
private ContentProviderUtils contentProviderUtils;
|
private ContentProviderUtils contentProviderUtils;
|
||||||
|
|
||||||
private RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
|
private RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
|
||||||
|
|
||||||
private Track track;
|
private Track.Id trackId;
|
||||||
private MarkerResourceCursorAdapter resourceCursorAdapter;
|
|
||||||
|
private MarkerListAdapter adapter;
|
||||||
|
|
||||||
private MarkerListBinding viewBinding;
|
private MarkerListBinding viewBinding;
|
||||||
|
|
||||||
private final MarkerLoaderCallback loaderCallbacks = new MarkerLoaderCallback();
|
|
||||||
|
|
||||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||||
|
|
||||||
private final TrackRecordingServiceConnection.Callback bindCallback = (service, unused) -> service.getRecordingStatusObservable()
|
private final TrackRecordingServiceConnection.Callback bindCallback = (service, unused) -> service.getRecordingStatusObservable()
|
||||||
@@ -105,33 +96,24 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
|||||||
private MenuItem insertMarkerMenuItem;
|
private MenuItem insertMarkerMenuItem;
|
||||||
private MenuItem searchMenuItem;
|
private MenuItem searchMenuItem;
|
||||||
|
|
||||||
|
private String searchQuery;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
|
|
||||||
Track.Id trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
|
|
||||||
|
|
||||||
contentProviderUtils = new ContentProviderUtils(this);
|
contentProviderUtils = new ContentProviderUtils(this);
|
||||||
|
|
||||||
track = trackId != null ? contentProviderUtils.getTrack(trackId) : null;
|
trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
|
||||||
|
|
||||||
viewBinding.markerList.setEmptyView(viewBinding.markerListEmpty);
|
|
||||||
viewBinding.markerList.setOnItemClickListener((parent, view, position, id) -> {
|
|
||||||
resourceCursorAdapter.markerInvalid(id);
|
|
||||||
Intent intent = IntentUtils.newIntent(MarkerListActivity.this, MarkerDetailActivity.class)
|
|
||||||
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, new Marker.Id(id));
|
|
||||||
startActivity(intent);
|
|
||||||
});
|
|
||||||
|
|
||||||
resourceCursorAdapter = new MarkerResourceCursorAdapter(this, R.layout.list_item);
|
|
||||||
ScrollVisibleViews scrollVisibleViews = new ScrollVisibleViews(resourceCursorAdapter);
|
|
||||||
viewBinding.markerList.setOnScrollListener(scrollVisibleViews);
|
|
||||||
viewBinding.markerList.setAdapter(resourceCursorAdapter);
|
|
||||||
ActivityUtils.configureListViewContextualMenu(viewBinding.markerList, contextualActionModeCallback);
|
|
||||||
|
|
||||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindCallback);
|
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindCallback);
|
||||||
|
|
||||||
|
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||||
|
adapter = new MarkerListAdapter(this, viewBinding.markerList, null);
|
||||||
|
viewBinding.markerList.setLayoutManager(layoutManager);
|
||||||
|
viewBinding.markerList.setAdapter(adapter);
|
||||||
|
|
||||||
setSupportActionBar(viewBinding.markerListToolbar);
|
setSupportActionBar(viewBinding.markerListToolbar);
|
||||||
|
adapter.setActionModeCallback(contextualActionModeCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -145,7 +127,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
|||||||
super.onResume();
|
super.onResume();
|
||||||
trackRecordingServiceConnection.bind(this);
|
trackRecordingServiceConnection.bind(this);
|
||||||
this.invalidateOptionsMenu();
|
this.invalidateOptionsMenu();
|
||||||
loadData(getIntent());
|
loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -157,9 +139,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
|||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
resourceCursorAdapter.clear();
|
|
||||||
viewBinding = null;
|
viewBinding = null;
|
||||||
resourceCursorAdapter = null;
|
adapter = null;
|
||||||
contentProviderUtils = null;
|
contentProviderUtils = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,15 +164,15 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||||
insertMarkerMenuItem.setVisible(track != null && track.getId().equals(recordingStatus.getTrackId()));
|
insertMarkerMenuItem.setVisible(trackId != null && trackId.equals(recordingStatus.getTrackId()));
|
||||||
return super.onPrepareOptionsMenu(menu);
|
return super.onPrepareOptionsMenu(menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||||
if (track != null && item.getItemId() == R.id.marker_list_insert_marker) {
|
if (trackId != null && item.getItemId() == R.id.marker_list_insert_marker) {
|
||||||
Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
|
Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
|
||||||
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, track.getId());
|
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -229,7 +210,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
|||||||
|
|
||||||
if (itemId == R.id.list_context_menu_edit) {
|
if (itemId == R.id.list_context_menu_edit) {
|
||||||
if (markerIds.length == 1) {
|
if (markerIds.length == 1) {
|
||||||
resourceCursorAdapter.markerInvalid(markerIds[0].id());
|
|
||||||
Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
|
Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
|
||||||
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerIds[0]);
|
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerIds[0]);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
@@ -243,23 +223,13 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (itemId == R.id.list_context_menu_select_all) {
|
if (itemId == R.id.list_context_menu_select_all) {
|
||||||
for (int i = 0; i < viewBinding.markerList.getCount(); i++) {
|
adapter.setAllSelected(true);
|
||||||
viewBinding.markerList.setItemChecked(i, true);
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
|
||||||
if (keyCode == KeyEvent.KEYCODE_SEARCH && searchMenuItem != null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onKeyUp(keyCode, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||||
@@ -267,8 +237,9 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
|||||||
searchView.setIconified(true);
|
searchView.setIconified(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loaderCallbacks.getSearchQuery() != null) {
|
if (searchQuery != null) {
|
||||||
loaderCallbacks.setSearch(null);
|
searchQuery = null;
|
||||||
|
loadData();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,78 +247,33 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNewIntent(Intent intent) {
|
protected void onNewIntent(Intent intent) {
|
||||||
super.onNewIntent(intent);
|
super.onNewIntent(intent);
|
||||||
setIntent(intent);
|
|
||||||
loadData(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadData(Intent intent) {
|
|
||||||
String searchQuery = null;
|
|
||||||
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
|
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
|
||||||
searchQuery = intent.getStringExtra(SearchManager.QUERY);
|
searchQuery = intent.getStringExtra(SearchManager.QUERY);
|
||||||
|
} else {
|
||||||
|
searchQuery = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
loaderCallbacks.setSearch(searchQuery);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void loadData() {
|
||||||
public void onMarkerDeleted() {
|
viewBinding.markerListToolbar.setTitle(Objects.requireNonNullElseGet(searchQuery, () -> getString(R.string.menu_markers)));
|
||||||
// Do nothing
|
|
||||||
|
List<Marker> markers = contentProviderUtils.searchMarkers(trackId, searchQuery);
|
||||||
|
|
||||||
|
adapter.swapData(markers);
|
||||||
|
|
||||||
|
if (markers.isEmpty()) {
|
||||||
|
viewBinding.markerListEmpty.setVisibility(View.VISIBLE);
|
||||||
|
viewBinding.markerList.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
viewBinding.markerListEmpty.setVisibility(View.GONE);
|
||||||
|
viewBinding.markerList.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onRecordingStatusChanged(RecordingStatus status) {
|
private void onRecordingStatusChanged(RecordingStatus status) {
|
||||||
recordingStatus = status;
|
recordingStatus = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MarkerLoaderCallback implements LoaderManager.LoaderCallbacks<Cursor> {
|
|
||||||
|
|
||||||
private String searchQuery = null;
|
|
||||||
|
|
||||||
public String getSearchQuery() {
|
|
||||||
return searchQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSearch(String searchQuery) {
|
|
||||||
this.searchQuery = searchQuery;
|
|
||||||
restart();
|
|
||||||
viewBinding.markerListToolbar.setTitle(Objects.requireNonNullElseGet(searchQuery, () -> getString(R.string.menu_markers)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void restart() {
|
|
||||||
LoaderManager.getInstance(MarkerListActivity.this).restartLoader(0, null, loaderCallbacks);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
|
|
||||||
final String[] PROJECTION = new String[]{MarkerColumns._ID,
|
|
||||||
MarkerColumns.NAME, MarkerColumns.DESCRIPTION, MarkerColumns.CATEGORY,
|
|
||||||
MarkerColumns.TIME, MarkerColumns.PHOTOURL, MarkerColumns.TRACKID};
|
|
||||||
|
|
||||||
if (searchQuery == null) {
|
|
||||||
if (track != null) {
|
|
||||||
return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, MarkerColumns.TRACKID + "=?", new String[]{String.valueOf(track.getId().id())}, null);
|
|
||||||
} else {
|
|
||||||
return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, null, null, null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final String SEARCH_QUERY = MarkerColumns.NAME + " LIKE ? OR " +
|
|
||||||
MarkerColumns.DESCRIPTION + " LIKE ? OR " +
|
|
||||||
MarkerColumns.CATEGORY + " LIKE ?";
|
|
||||||
final String[] selectionArgs = new String[]{"%" + searchQuery + "%", "%" + searchQuery + "%", "%" + searchQuery + "%"};
|
|
||||||
return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, SEARCH_QUERY, selectionArgs, MarkerColumns.DEFAULT_SORT_ORDER + " DESC");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
|
|
||||||
resourceCursorAdapter.swapCursor(cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
|
||||||
resourceCursorAdapter.swapCursor(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
package de.dennisguse.opentracks.ui.markers;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.media.ThumbnailUtils;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.collection.LruCache;
|
|
||||||
import androidx.cursoradapter.widget.ResourceCursorAdapter;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
|
||||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
|
||||||
import de.dennisguse.opentracks.data.models.Track;
|
|
||||||
import de.dennisguse.opentracks.data.tables.MarkerColumns;
|
|
||||||
import de.dennisguse.opentracks.ui.util.ExecutorListViewService;
|
|
||||||
import de.dennisguse.opentracks.ui.util.ListItemUtils;
|
|
||||||
import de.dennisguse.opentracks.ui.util.ScrollVisibleViews;
|
|
||||||
|
|
||||||
public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implements ScrollVisibleViews.VisibleViewsListener {
|
|
||||||
|
|
||||||
private static final String TAG = MarkerResourceCursorAdapter.class.getSimpleName();
|
|
||||||
|
|
||||||
private static final int LIST_PREFERRED_ITEM_HEIGHT_DEFAULT = 128;
|
|
||||||
|
|
||||||
private final Activity activity;
|
|
||||||
|
|
||||||
//TODO Should be Marker.Id
|
|
||||||
private final ExecutorListViewService<Long> executorService = new ExecutorListViewService<>(LIST_PREFERRED_ITEM_HEIGHT_DEFAULT);
|
|
||||||
|
|
||||||
private boolean scroll = false;
|
|
||||||
|
|
||||||
// Cache size is in bytes.
|
|
||||||
private final LruCache<String, Bitmap> memoryCache;
|
|
||||||
|
|
||||||
public MarkerResourceCursorAdapter(Activity activity, int layout) {
|
|
||||||
super(activity, layout, null, 0);
|
|
||||||
|
|
||||||
this.activity = activity;
|
|
||||||
|
|
||||||
// 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(@NonNull String key, @NonNull Bitmap bitmap) {
|
|
||||||
return bitmap.getByteCount();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void bindView(View view, Context context, Cursor cursor) {
|
|
||||||
int idIndex = cursor.getColumnIndex(MarkerColumns._ID);
|
|
||||||
int nameIndex = cursor.getColumnIndex(MarkerColumns.NAME);
|
|
||||||
int timeIndex = cursor.getColumnIndexOrThrow(MarkerColumns.TIME);
|
|
||||||
int categoryIndex = cursor.getColumnIndex(MarkerColumns.CATEGORY);
|
|
||||||
int descriptionIndex = cursor.getColumnIndex(MarkerColumns.DESCRIPTION);
|
|
||||||
int photoUrlIndex = cursor.getColumnIndex(MarkerColumns.PHOTOURL);
|
|
||||||
int trackIdIndex = cursor.getColumnIndex(MarkerColumns.TRACKID);
|
|
||||||
|
|
||||||
long id = cursor.getLong(idIndex);
|
|
||||||
int iconId = MarkerUtils.ICON_ID;
|
|
||||||
String name = cursor.getString(nameIndex);
|
|
||||||
long time = cursor.getLong(timeIndex);
|
|
||||||
String category = cursor.getString(categoryIndex);
|
|
||||||
String description = cursor.getString(descriptionIndex);
|
|
||||||
String photoUrl = cursor.getString(photoUrlIndex);
|
|
||||||
long trackId = cursor.getLong(trackIdIndex);
|
|
||||||
|
|
||||||
view.setTag(String.valueOf(id));
|
|
||||||
|
|
||||||
boolean hasPhoto = photoUrl != null && !photoUrl.equals("");
|
|
||||||
|
|
||||||
ImageView imageView = view.findViewById(R.id.list_item_photo);
|
|
||||||
ImageView textGradient = view.findViewById(R.id.list_item_text_gradient);
|
|
||||||
imageView.setVisibility(hasPhoto ? View.VISIBLE : View.GONE);
|
|
||||||
textGradient.setVisibility(hasPhoto ? View.VISIBLE : View.GONE);
|
|
||||||
imageView.setImageBitmap(null);
|
|
||||||
|
|
||||||
if (hasPhoto) {
|
|
||||||
int height = getPhotoHeight(activity);
|
|
||||||
ViewGroup.LayoutParams params = imageView.getLayoutParams();
|
|
||||||
params.height = height;
|
|
||||||
imageView.setLayoutParams(params);
|
|
||||||
|
|
||||||
if (getBitmapFromMemCache(String.valueOf(id)) != null || !scroll) {
|
|
||||||
asyncLoadPhoto(view, imageView, photoUrl, id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
|
||||||
Track track = contentProviderUtils.getTrack(new Track.Id(trackId));
|
|
||||||
ListItemUtils.setListItem(activity, view, false, iconId, R.string.image_marker, name, null, null, 0,
|
|
||||||
OffsetDateTime.ofInstant(Instant.ofEpochMilli(time), track.getZoneOffset()), category, description, hasPhoto);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear() {
|
|
||||||
executorService.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewVisible(View view, int position) {
|
|
||||||
scroll = true;
|
|
||||||
|
|
||||||
Cursor cursor = getCursor();
|
|
||||||
if (!cursor.moveToPosition(position)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MarkerColumns._ID));
|
|
||||||
String photoUrl = cursor.getString(cursor.getColumnIndexOrThrow(MarkerColumns.PHOTOURL));
|
|
||||||
|
|
||||||
boolean hasPhoto = photoUrl != null && !photoUrl.equals("");
|
|
||||||
if (hasPhoto) {
|
|
||||||
ImageView imageView = view.findViewById(R.id.list_item_photo);
|
|
||||||
asyncLoadPhoto(view, imageView, photoUrl, id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void markerInvalid(long id) {
|
|
||||||
memoryCache.remove(String.valueOf(id));
|
|
||||||
scroll = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
|
|
||||||
synchronized (memoryCache) {
|
|
||||||
if (getBitmapFromMemCache(key) == null) {
|
|
||||||
memoryCache.put(key, bitmap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Bitmap getBitmapFromMemCache(String key) {
|
|
||||||
return memoryCache.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 view item's view.
|
|
||||||
* @param imageView view object where photo will be loaded.
|
|
||||||
* @param photoUrl photo's url.
|
|
||||||
* @param id marker's id where photo belong.
|
|
||||||
*/
|
|
||||||
private void asyncLoadPhoto(View view, ImageView imageView, String photoUrl, long id) {
|
|
||||||
Bitmap photo = getBitmapFromMemCache(String.valueOf(id));
|
|
||||||
imageView.setImageBitmap(photo);
|
|
||||||
|
|
||||||
if (photo == null) {
|
|
||||||
executorService.execute(id, () -> {
|
|
||||||
try (InputStream inputStream = activity.getContentResolver().openInputStream(Uri.parse(photoUrl))) {
|
|
||||||
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
|
|
||||||
int height = getPhotoHeight(activity);
|
|
||||||
Log.d(TAG, "Width : " + (bitmap.getWidth() / (bitmap.getHeight() / height)) + " | Height: " + height);
|
|
||||||
final Bitmap finalPhoto = ThumbnailUtils.extractThumbnail(bitmap, bitmap.getWidth() / (bitmap.getHeight() / height), height);
|
|
||||||
addBitmapToMemoryCache(String.valueOf(id), finalPhoto);
|
|
||||||
if (view.getTag().equals(String.valueOf(id))) {
|
|
||||||
activity.runOnUiThread(() -> imageView.setImageBitmap(finalPhoto));
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(TAG, "Failed to image " + e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the photo height.
|
|
||||||
*
|
|
||||||
* @param context the context
|
|
||||||
*/
|
|
||||||
private static int getPhotoHeight(Context context) {
|
|
||||||
int[] attrs = new int[]{android.R.attr.listPreferredItemHeight};
|
|
||||||
TypedArray typeArray = context.obtainStyledAttributes(attrs);
|
|
||||||
int height = typeArray.getDimensionPixelSize(0, LIST_PREFERRED_ITEM_HEIGHT_DEFAULT);
|
|
||||||
typeArray.recycle();
|
|
||||||
return 2 * height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,9 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
@@ -180,4 +182,17 @@ public class ListItemUtils {
|
|||||||
textView.setShadowLayer(0, 0, 0, 0);
|
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();
|
typedArray.recycle();
|
||||||
return fontSize;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ limitations under the License.
|
|||||||
android:paddingBottom="?attr/actionBarSize"
|
android:paddingBottom="?attr/actionBarSize"
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
<ListView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/marker_list"
|
android:id="@+id/marker_list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|||||||
@@ -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