Merge pull request #3937 from ByteHamster/recyclerview

Converted lists to RecyclerView
This commit is contained in:
H. Lehmann 2020-03-17 15:41:24 +01:00 committed by GitHub
commit 54056dd8d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 681 additions and 957 deletions

View File

@ -34,6 +34,8 @@ import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.action.ViewActions.swipeUp;
import static androidx.test.espresso.intent.Intents.intended;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent;
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
@ -80,52 +82,51 @@ public class NavigationDrawerTest {
uiTestUtils.addLocalFeedData(false);
UserPreferences.setHiddenDrawerItems(new ArrayList<>());
activityRule.launchActivity(new Intent());
MainActivity activity = activityRule.getActivity();
// queue
openNavDrawer();
onDrawerItem(withText(R.string.queue_label)).perform(click());
onView(isRoot()).perform(waitForView(withId(R.id.recyclerView), 1000));
assertEquals(activity.getString(R.string.queue_label), activity.getSupportActionBar().getTitle());
onView(isRoot()).perform(waitForView(allOf(isDescendantOfA(withId(R.id.toolbar)),
withText(R.string.queue_label)), 1000));
// episodes
openNavDrawer();
onDrawerItem(withText(R.string.episodes_label)).perform(click());
onView(isRoot()).perform(waitForView(withId(android.R.id.list), 1000));
assertEquals(activity.getString(R.string.episodes_label), activity.getSupportActionBar().getTitle());
onView(isRoot()).perform(waitForView(allOf(isDescendantOfA(withId(R.id.toolbar)),
withText(R.string.episodes_label), isDisplayed()), 1000));
// Subscriptions
openNavDrawer();
onDrawerItem(withText(R.string.subscriptions_label)).perform(click());
onView(isRoot()).perform(waitForView(withId(R.id.subscriptions_grid), 1000));
assertEquals(activity.getString(R.string.subscriptions_label), activity.getSupportActionBar().getTitle());
onView(isRoot()).perform(waitForView(allOf(isDescendantOfA(withId(R.id.toolbar)),
withText(R.string.subscriptions_label), isDisplayed()), 1000));
// downloads
openNavDrawer();
onDrawerItem(withText(R.string.downloads_label)).perform(click());
onView(isRoot()).perform(waitForView(withId(android.R.id.list), 1000));
assertEquals(activity.getString(R.string.downloads_label), activity.getSupportActionBar().getTitle());
onView(isRoot()).perform(waitForView(allOf(isDescendantOfA(withId(R.id.toolbar)),
withText(R.string.downloads_label), isDisplayed()), 1000));
// playback history
openNavDrawer();
onDrawerItem(withText(R.string.playback_history_label)).perform(click());
onView(isRoot()).perform(waitForView(withId(android.R.id.list), 1000));
assertEquals(activity.getString(R.string.playback_history_label), activity.getSupportActionBar().getTitle());
onView(isRoot()).perform(waitForView(allOf(isDescendantOfA(withId(R.id.toolbar)),
withText(R.string.playback_history_label), isDisplayed()), 1000));
// add podcast
openNavDrawer();
onView(withId(R.id.nav_list)).perform(swipeUp());
onDrawerItem(withText(R.string.add_feed_label)).perform(click());
onView(isRoot()).perform(waitForView(withId(R.id.btn_add_via_url), 1000));
assertEquals(activity.getString(R.string.add_feed_label), activity.getSupportActionBar().getTitle());
onView(isRoot()).perform(waitForView(allOf(isDescendantOfA(withId(R.id.toolbar)),
withText(R.string.add_feed_label), isDisplayed()), 1000));
// podcasts
for (int i = 0; i < uiTestUtils.hostedFeeds.size(); i++) {
Feed f = uiTestUtils.hostedFeeds.get(i);
openNavDrawer();
onDrawerItem(withText(f.getTitle())).perform(scrollTo(), click());
onView(isRoot()).perform(waitForView(withId(android.R.id.list), 1000));
assertEquals("", activity.getSupportActionBar().getTitle());
onView(isRoot()).perform(waitForView(allOf(isDescendantOfA(withId(R.id.appBar)),
withText(f.getTitle()), isDisplayed()), 1000));
}
}

View File

@ -28,7 +28,6 @@ import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.ThemeUtils;
import de.danoeh.antennapod.view.viewholder.DownloadItemViewHolder;
import de.danoeh.antennapod.view.viewholder.FeedViewHolder;
/**
* Displays a list of DownloadStatus entries.

View File

@ -1,67 +0,0 @@
package de.danoeh.antennapod.adapter;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.util.ThemeUtils;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
/**
* Shows a list of downloaded episodes.
*/
public class DownloadedEpisodesListAdapter extends BaseAdapter {
private final MainActivity activity;
private final ItemAccess itemAccess;
public DownloadedEpisodesListAdapter(MainActivity activity, ItemAccess itemAccess) {
super();
this.activity = activity;
this.itemAccess = itemAccess;
}
@Override
public int getCount() {
return itemAccess.getCount();
}
@Override
public FeedItem getItem(int position) {
return itemAccess.getItem(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
EpisodeItemViewHolder holder;
if (convertView == null) {
holder = new EpisodeItemViewHolder(activity, parent);
} else {
holder = (EpisodeItemViewHolder) convertView.getTag();
}
final FeedItem item = getItem(position);
holder.bind(item);
holder.dragHandle.setVisibility(View.GONE);
holder.secondaryActionIcon.setImageResource(ThemeUtils.getDrawableFromAttr(activity, R.attr.ic_delete));
holder.secondaryActionButton.setOnClickListener(v -> itemAccess.onFeedItemSecondaryAction(item));
holder.hideSeparatorIfNecessary();
return holder.itemView;
}
public interface ItemAccess {
int getCount();
FeedItem getItem(int position);
void onFeedItemSecondaryAction(FeedItem item);
}
}

View File

@ -1,12 +1,12 @@
package de.danoeh.antennapod.adapter;
import android.app.Activity;
import android.view.ContextMenu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
@ -24,17 +24,17 @@ import java.util.List;
/**
* List adapter for the list of new episodes.
*/
public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<EpisodeItemViewHolder>
public class EpisodeItemListAdapter extends RecyclerView.Adapter<EpisodeItemViewHolder>
implements View.OnCreateContextMenuListener {
private final WeakReference<MainActivity> mainActivityRef;
private List<FeedItem> episodes = new ArrayList<>();
private FeedItem selectedItem;
public AllEpisodesRecycleAdapter(MainActivity mainActivity) {
public EpisodeItemListAdapter(MainActivity mainActivity) {
super();
this.mainActivityRef = new WeakReference<>(mainActivity);
setHasStableIds(true);
}
public void updateItems(List<FeedItem> items) {
@ -45,9 +45,7 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<EpisodeItemV
@NonNull
@Override
public EpisodeItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
EpisodeItemViewHolder viewHolder = new EpisodeItemViewHolder(mainActivityRef.get(), parent);
viewHolder.dragHandle.setVisibility(View.GONE);
return viewHolder;
return new EpisodeItemViewHolder(mainActivityRef.get(), parent);
}
@Override
@ -86,6 +84,14 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<EpisodeItemV
return episodes.size();
}
protected FeedItem getItem(int index) {
return episodes.get(index);
}
protected Activity getActivity() {
return mainActivityRef.get();
}
@Override
public void onCreateContextMenu(final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
MenuInflater inflater = mainActivityRef.get().getMenuInflater();
@ -93,26 +99,4 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<EpisodeItemV
menu.setHeaderTitle(selectedItem.getTitle());
FeedItemMenuHandler.onPrepareMenu(menu, selectedItem, R.id.skip_episode_item);
}
/**
* Notifies a View Holder of relevant callbacks from
* {@link ItemTouchHelper.Callback}.
*/
public interface ItemTouchHelperViewHolder {
/**
* Called when the {@link ItemTouchHelper} first registers an
* item as being moved or swiped.
* Implementations should update the item view to indicate
* it's active state.
*/
void onItemSelected();
/**
* Called when the {@link ItemTouchHelper} has completed the
* move or swipe, and the active item state should be cleared.
*/
void onItemClear();
}
}

View File

@ -1,119 +0,0 @@
package de.danoeh.antennapod.adapter;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedComponent;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
import de.danoeh.antennapod.view.viewholder.FeedComponentViewHolder;
import de.danoeh.antennapod.view.viewholder.FeedViewHolder;
/**
* List adapter for items of feeds that the user has already subscribed to.
*/
public class FeedItemlistAdapter extends BaseAdapter {
private final ItemAccess itemAccess;
private final MainActivity activity;
private final boolean makePlayedItemsTransparent;
private final boolean showIcons;
private int currentlyPlayingItem = -1;
public FeedItemlistAdapter(MainActivity activity, ItemAccess itemAccess,
boolean showIcons, boolean makePlayedItemsTransparent) {
super();
this.activity = activity;
this.itemAccess = itemAccess;
this.showIcons = showIcons;
this.makePlayedItemsTransparent = makePlayedItemsTransparent;
}
@Override
public int getCount() {
return itemAccess.getCount();
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public FeedComponent getItem(int position) {
return itemAccess.getItem(position);
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
final FeedComponent item = getItem(position);
if (item instanceof Feed) {
return getView((Feed) item, convertView, parent);
} else {
final FeedItem feeditem = (FeedItem) item;
if (feeditem.getMedia() != null && feeditem.getMedia().isCurrentlyPlaying()) {
currentlyPlayingItem = position;
}
return getView(feeditem, convertView, parent);
}
}
private View getView(Feed item, View convertView, ViewGroup parent) {
FeedViewHolder holder;
if (convertView == null || !(convertView.getTag() instanceof FeedViewHolder)) {
holder = new FeedViewHolder(activity, parent);
} else {
holder = (FeedViewHolder) convertView.getTag();
}
holder.bind(item);
return holder.itemView;
}
private View getView(final FeedItem item, View convertView, ViewGroup parent) {
EpisodeItemViewHolder holder;
if (convertView == null || !(convertView.getTag() instanceof EpisodeItemViewHolder)) {
holder = new EpisodeItemViewHolder(activity, parent);
} else {
holder = (EpisodeItemViewHolder) convertView.getTag();
}
if (!showIcons) {
holder.coverHolder.setVisibility(View.GONE);
}
holder.bind(item);
holder.dragHandle.setVisibility(View.GONE);
if (!makePlayedItemsTransparent) {
holder.itemView.setAlpha(1.0f);
}
holder.hideSeparatorIfNecessary();
return holder.itemView;
}
public void notifyCurrentlyPlayingItemChanged(PlaybackPositionEvent event, ListView listView) {
if (currentlyPlayingItem != -1 && currentlyPlayingItem < getCount()) {
View view = listView.getChildAt(currentlyPlayingItem
- listView.getFirstVisiblePosition() + listView.getHeaderViewsCount());
if (view == null) {
return;
}
EpisodeItemViewHolder holder = (EpisodeItemViewHolder) view.getTag();
holder.notifyPlaybackPositionUpdated(event);
}
}
public interface ItemAccess {
int getCount();
FeedComponent getItem(int position);
}
}

View File

@ -0,0 +1,76 @@
package de.danoeh.antennapod.adapter;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.fragment.FeedItemlistFragment;
import de.danoeh.antennapod.view.SquareImageView;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class FeedSearchResultAdapter extends RecyclerView.Adapter<FeedSearchResultAdapter.Holder> {
private final WeakReference<MainActivity> mainActivityRef;
private final List<Feed> data = new ArrayList<>();
public FeedSearchResultAdapter(MainActivity mainActivity) {
this.mainActivityRef = new WeakReference<>(mainActivity);
}
public void updateData(List<Feed> newData) {
data.clear();
data.addAll(newData);
notifyDataSetChanged();
}
@NonNull
@Override
public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View convertView = View.inflate(mainActivityRef.get(), R.layout.searchlist_item_feed, null);
return new Holder(convertView);
}
@Override
public void onBindViewHolder(@NonNull Holder holder, int position) {
final Feed podcast = data.get(position);
holder.imageView.setContentDescription(podcast.getTitle());
holder.imageView.setOnClickListener(v ->
mainActivityRef.get().loadChildFragment(FeedItemlistFragment.newInstance(podcast.getId())));
Glide.with(mainActivityRef.get())
.load(podcast.getImageUrl())
.apply(new RequestOptions()
.placeholder(R.color.light_gray)
.fitCenter()
.dontAnimate())
.into(holder.imageView);
}
@Override
public long getItemId(int position) {
return data.get(position).getId();
}
@Override
public int getItemCount() {
return data.size();
}
static class Holder extends RecyclerView.ViewHolder {
SquareImageView imageView;
public Holder(@NonNull View itemView) {
super(itemView);
imageView = itemView.findViewById(R.id.discovery_cover);
imageView.setDirection(SquareImageView.DIRECTION_HEIGHT);
}
}
}

View File

@ -6,44 +6,24 @@ import android.view.ContextMenu;
import android.view.MenuInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.MotionEventCompat;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.fragment.ItemPagerFragment;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
import org.apache.commons.lang3.ArrayUtils;
import java.lang.ref.WeakReference;
/**
* List adapter for the queue.
*/
public class QueueRecyclerAdapter extends RecyclerView.Adapter<EpisodeItemViewHolder> implements View.OnCreateContextMenuListener {
public class QueueRecyclerAdapter extends EpisodeItemListAdapter {
private static final String TAG = "QueueRecyclerAdapter";
private final WeakReference<MainActivity> mainActivity;
private final ItemAccess itemAccess;
private final ItemTouchHelper itemTouchHelper;
private boolean locked;
private FeedItem selectedItem;
public QueueRecyclerAdapter(MainActivity mainActivity,
ItemAccess itemAccess,
ItemTouchHelper itemTouchHelper) {
super();
this.mainActivity = new WeakReference<>(mainActivity);
this.itemAccess = itemAccess;
public QueueRecyclerAdapter(MainActivity mainActivity, ItemTouchHelper itemTouchHelper) {
super(mainActivity);
this.itemTouchHelper = itemTouchHelper;
locked = UserPreferences.isQueueLocked();
}
@ -53,31 +33,10 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter<EpisodeItemViewHo
notifyDataSetChanged();
}
@NonNull
@Override
public EpisodeItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new EpisodeItemViewHolder(mainActivity.get(), parent);
}
@Override
@SuppressLint("ClickableViewAccessibility")
public void onBindViewHolder(EpisodeItemViewHolder holder, int pos) {
FeedItem item = itemAccess.getItem(pos);
holder.bind(item);
holder.dragHandle.setVisibility(locked ? View.GONE : View.VISIBLE);
holder.itemView.setOnLongClickListener(v -> {
selectedItem = item;
return false;
});
holder.itemView.setOnClickListener(v -> {
MainActivity activity = mainActivity.get();
if (activity != null) {
long[] ids = itemAccess.getQueueIds().toArray();
int position = ArrayUtils.indexOf(ids, item.getId());
activity.loadChildFragment(ItemPagerFragment.newInstance(ids, position));
}
});
super.onBindViewHolder(holder, pos);
View.OnTouchListener startDragTouchListener = (v1, event) -> {
if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
Log.d(TAG, "startDrag()");
@ -85,80 +44,33 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter<EpisodeItemViewHo
}
return false;
};
if (!locked) {
holder.dragHandle.setOnTouchListener(startDragTouchListener);
holder.coverHolder.setOnTouchListener(startDragTouchListener);
} else {
if (locked) {
holder.dragHandle.setVisibility(View.GONE);
holder.dragHandle.setOnTouchListener(null);
holder.coverHolder.setOnTouchListener(null);
} else {
holder.dragHandle.setVisibility(View.VISIBLE);
holder.dragHandle.setOnTouchListener(startDragTouchListener);
holder.coverHolder.setOnTouchListener(startDragTouchListener);
}
holder.itemView.setOnCreateContextMenuListener(this);
holder.isInQueue.setVisibility(View.GONE);
holder.hideSeparatorIfNecessary();
}
@Nullable
public FeedItem getSelectedItem() {
return selectedItem;
}
@Override
public long getItemId(int position) {
FeedItem item = itemAccess.getItem(position);
return item != null ? item.getId() : RecyclerView.NO_POSITION;
}
public int getItemCount() {
return itemAccess.getCount();
}
@Override
public void onCreateContextMenu(final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
MenuInflater inflater = mainActivity.get().getMenuInflater();
inflater.inflate(R.menu.queue_context, menu); // queue-specific menu items
inflater.inflate(R.menu.feeditemlist_context, menu); // generic menu items for item feeds
MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.queue_context, menu);
super.onCreateContextMenu(menu, v, menuInfo);
menu.setHeaderTitle(selectedItem.getTitle());
FeedItemMenuHandler.onPrepareMenu(menu, selectedItem, R.id.skip_episode_item);
// Queue-specific menu preparation
final boolean keepSorted = UserPreferences.isQueueKeepSorted();
final LongList queueAccess = itemAccess.getQueueIds();
if (queueAccess.size() == 0 || queueAccess.get(0) == selectedItem.getId() || keepSorted) {
if (getItem(0).getId() == getSelectedItem().getId() || keepSorted) {
menu.findItem(R.id.move_to_top_item).setVisible(false);
}
if (queueAccess.size() == 0 || queueAccess.get(queueAccess.size() - 1) == selectedItem.getId() || keepSorted) {
if (getItem(getItemCount() - 1).getId() == getSelectedItem().getId() || keepSorted) {
menu.findItem(R.id.move_to_bottom_item).setVisible(false);
}
}
public interface ItemAccess {
FeedItem getItem(int position);
int getCount();
LongList getQueueIds();
}
/**
* Notifies a View Holder of relevant callbacks from
* {@link ItemTouchHelper.Callback}.
*/
public interface ItemTouchHelperViewHolder {
/**
* Called when the {@link ItemTouchHelper} first registers an
* item as being moved or swiped.
* Implementations should update the item view to indicate
* it's active state.
*/
void onItemSelected();
/**
* Called when the {@link ItemTouchHelper} has completed the
* move or swipe, and the active item state should be cleared.
*/
void onItemClear();
}
}

View File

@ -1,61 +1,81 @@
package de.danoeh.antennapod.fragment;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.ListFragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ListView;
import java.util.ArrayList;
import java.util.List;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.DownloadedEpisodesListAdapter;
import de.danoeh.antennapod.adapter.EpisodeItemListAdapter;
import de.danoeh.antennapod.adapter.actionbutton.DeleteActionButton;
import de.danoeh.antennapod.core.event.DownloadLogEvent;
import de.danoeh.antennapod.core.event.FeedItemEvent;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.event.PlayerStatusEvent;
import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.view.EmptyViewHandler;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
import java.util.List;
import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_ADD_TO_QUEUE;
import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_DELETE;
/**
* Displays all running downloads and provides a button to delete them
* Displays all completed downloads and provides a button to delete them.
*/
public class CompletedDownloadsFragment extends ListFragment {
public class CompletedDownloadsFragment extends Fragment {
private static final String TAG = CompletedDownloadsFragment.class.getSimpleName();
private List<FeedItem> items = new ArrayList<>();
private DownloadedEpisodesListAdapter listAdapter;
private CompletedDownloadsListAdapter adapter;
private RecyclerView recyclerView;
private Disposable disposable;
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setHasOptionsMenu(true);
addVerticalPadding();
addEmptyView();
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.simple_list_fragment, container, false);
Toolbar toolbar = root.findViewById(R.id.toolbar);
toolbar.setVisibility(View.GONE);
listAdapter = new DownloadedEpisodesListAdapter((MainActivity) getActivity(), itemAccess);
setListAdapter(listAdapter);
setListShown(false);
recyclerView = root.findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
recyclerView.setHasFixedSize(true);
recyclerView.addItemDecoration(new HorizontalDividerItemDecoration.Builder(getActivity()).build());
recyclerView.setVisibility(View.GONE);
adapter = new CompletedDownloadsListAdapter((MainActivity) getActivity());
recyclerView.setAdapter(adapter);
addEmptyView();
EventBus.getDefault().register(this);
return root;
}
@Override
@ -67,6 +87,7 @@ public class CompletedDownloadsFragment extends ListFragment {
@Override
public void onStart() {
super.onStart();
setHasOptionsMenu(true);
loadItems();
}
@ -78,14 +99,6 @@ public class CompletedDownloadsFragment extends ListFragment {
}
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
position -= l.getHeaderViewsCount();
long[] ids = FeedItemUtil.getIds(items);
((MainActivity) requireActivity()).loadChildFragment(ItemPagerFragment.newInstance(ids, position));
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
@ -103,41 +116,66 @@ public class CompletedDownloadsFragment extends ListFragment {
return false;
}
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
FeedItem selectedItem = adapter.getSelectedItem();
if (selectedItem == null) {
Log.i(TAG, "Selected item at current position was null, ignoring selection");
return super.onContextItemSelected(item);
}
return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem);
}
private void addEmptyView() {
EmptyViewHandler emptyView = new EmptyViewHandler(getActivity());
emptyView.setIcon(R.attr.av_download);
emptyView.setTitle(R.string.no_comp_downloads_head_label);
emptyView.setMessage(R.string.no_comp_downloads_label);
emptyView.attachToListView(getListView());
emptyView.attachToRecyclerView(recyclerView);
}
private void addVerticalPadding() {
final ListView lv = getListView();
lv.setClipToPadding(false);
final int vertPadding = getResources().getDimensionPixelSize(R.dimen.list_vertical_padding);
lv.setPadding(0, vertPadding, 0, vertPadding);
}
private final DownloadedEpisodesListAdapter.ItemAccess itemAccess = new DownloadedEpisodesListAdapter.ItemAccess() {
@Override
public int getCount() {
return items.size();
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(FeedItemEvent event) {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
if (items == null) {
return;
} else if (adapter == null) {
loadItems();
return;
}
@Override
public FeedItem getItem(int position) {
if (0 <= position && position < items.size()) {
return items.get(position);
} else {
return null;
for (int i = 0, size = event.items.size(); i < size; i++) {
FeedItem item = event.items.get(i);
int pos = FeedItemUtil.indexOfItemWithId(items, item.getId());
if (pos >= 0) {
items.remove(pos);
if (item.getMedia().isDownloaded()) {
items.add(pos, item);
adapter.notifyItemChanged(pos);
} else {
adapter.notifyItemRemoved(pos);
}
}
}
}
@Override
public void onFeedItemSecondaryAction(FeedItem item) {
DBWriter.deleteFeedMediaOfItem(requireActivity(), item.getMedia().getId());
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(PlaybackPositionEvent event) {
if (adapter != null) {
for (int i = 0; i < adapter.getItemCount(); i++) {
EpisodeItemViewHolder holder = (EpisodeItemViewHolder) recyclerView.findViewHolderForAdapterPosition(i);
if (holder != null && holder.isCurrentlyPlayingItem()) {
holder.notifyPlaybackPositionUpdated(event);
break;
}
}
}
};
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onPlayerStatusChanged(PlayerStatusEvent event) {
loadItems();
}
@Subscribe
public void onDownloadLogChanged(DownloadLogEvent event) {
@ -158,13 +196,22 @@ public class CompletedDownloadsFragment extends ListFragment {
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
items = result;
onItemsLoaded();
adapter.updateItems(result);
requireActivity().invalidateOptionsMenu();
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
}
private void onItemsLoaded() {
setListShown(true);
listAdapter.notifyDataSetChanged();
requireActivity().invalidateOptionsMenu();
private static class CompletedDownloadsListAdapter extends EpisodeItemListAdapter {
public CompletedDownloadsListAdapter(MainActivity mainActivity) {
super(mainActivity);
}
@Override
public void onBindViewHolder(EpisodeItemViewHolder holder, int pos) {
super.onBindViewHolder(holder, pos);
DeleteActionButton actionButton = new DeleteActionButton(getItem(pos));
actionButton.configure(holder.secondaryActionButton, holder.secondaryActionIcon, getActivity());
}
}
}

View File

@ -6,10 +6,8 @@ import android.content.SharedPreferences;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.core.view.MenuItemCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.appcompat.widget.SearchView;
import androidx.recyclerview.widget.SimpleItemAnimator;
import android.util.Log;
import android.view.LayoutInflater;
@ -24,6 +22,7 @@ import android.widget.Toast;
import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration;
import de.danoeh.antennapod.adapter.EpisodeItemListAdapter;
import de.danoeh.antennapod.core.event.FeedListUpdateEvent;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.event.PlayerStatusEvent;
@ -38,14 +37,12 @@ import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.AllEpisodesRecycleAdapter;
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.DownloaderUpdate;
import de.danoeh.antennapod.core.event.FeedItemEvent;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.FeedItemUtil;
@ -73,7 +70,7 @@ public abstract class EpisodesListFragment extends Fragment {
protected int page = 1;
RecyclerView recyclerView;
AllEpisodesRecycleAdapter listAdapter;
EpisodeItemListAdapter listAdapter;
ProgressBar progLoading;
View loadingMore;
EmptyViewHandler emptyView;
@ -346,8 +343,7 @@ public abstract class EpisodesListFragment extends Fragment {
*/
private void createRecycleAdapter(RecyclerView recyclerView, EmptyViewHandler emptyViewHandler) {
MainActivity mainActivity = (MainActivity) getActivity();
listAdapter = new AllEpisodesRecycleAdapter(mainActivity);
listAdapter.setHasStableIds(true);
listAdapter = new EpisodeItemListAdapter(mainActivity);
listAdapter.updateItems(episodes);
recyclerView.setAdapter(listAdapter);
emptyViewHandler.updateAdapter(listAdapter);

View File

@ -5,7 +5,6 @@ import android.content.DialogInterface;
import android.graphics.LightingColorFilter;
import android.os.Bundle;
import android.util.Log;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@ -15,7 +14,6 @@ import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
@ -23,19 +21,20 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.MenuItemCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
import com.joanzapata.iconify.Iconify;
import com.joanzapata.iconify.widget.IconTextView;
import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.FeedItemlistAdapter;
import de.danoeh.antennapod.adapter.EpisodeItemListAdapter;
import de.danoeh.antennapod.core.asynctask.FeedRemover;
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
@ -68,6 +67,7 @@ import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.menuhandler.FeedMenuHandler;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.view.ToolbarIconTintManager;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
@ -86,12 +86,11 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
private static final String TAG = "ItemlistFragment";
private static final String ARGUMENT_FEED_ID = "argument.de.danoeh.antennapod.feed_id";
private FeedItemlistAdapter adapter;
private AdapterView.AdapterContextMenuInfo lastMenuInfo = null;
private MoreContentListFooterUtil listFooter;
private FeedItemListAdapter adapter;
private MoreContentListFooterUtil nextPageLoader;
private ProgressBar progressBar;
private ListView listView;
private RecyclerView recyclerView;
private TextView txtvTitle;
private IconTextView txtvFailure;
private ImageView imgvBackground;
@ -144,9 +143,13 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
toolbar.setTitle("");
((AppCompatActivity) getActivity()).setSupportActionBar(toolbar);
listView = root.findViewById(android.R.id.list);
listView.setOnItemClickListener(this);
registerForContextMenu(listView);
recyclerView = root.findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
recyclerView.setHasFixedSize(true);
recyclerView.addItemDecoration(new HorizontalDividerItemDecoration.Builder(getActivity()).build());
recyclerView.setVisibility(View.GONE);
progressBar = root.findViewById(R.id.progLoading);
txtvTitle = root.findViewById(R.id.txtvTitle);
txtvAuthor = root.findViewById(R.id.txtvAuthor);
@ -176,6 +179,33 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
}
};
appBar.addOnOffsetChangedListener(iconTintManager);
nextPageLoader = new MoreContentListFooterUtil(root.findViewById(R.id.more_content_list_footer));
nextPageLoader.setClickListener(() -> {
if (feed != null) {
try {
DBTasks.loadNextPageOfFeed(getActivity(), feed, false);
} catch (DownloadRequestException e) {
e.printStackTrace();
DownloadRequestErrorDialogCreator.newRequestErrorDialog(getActivity(), e.getMessage());
}
}
});
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int deltaX, int deltaY) {
super.onScrolled(recyclerView, deltaX, deltaY);
int visibleEpisodeCount = recyclerView.getChildCount();
int totalEpisodeCount = recyclerView.getLayoutManager().getItemCount();
int firstVisibleEpisode = layoutManager.findFirstVisibleItemPosition();
boolean isAtBottom = (totalEpisodeCount - visibleEpisodeCount) <= (firstVisibleEpisode + 3);
boolean hasMorePages = feed != null && feed.isPaged() && feed.getNextPageLink() != null;
nextPageLoader.getRoot().setVisibility((isAtBottom && hasMorePages) ? View.VISIBLE : View.GONE);
}
});
EventBus.getDefault().register(this);
loadItems();
return root;
@ -190,7 +220,6 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
disposable.dispose();
}
adapter = null;
listFooter = null;
}
private final MenuItemUtils.UpdateRefreshMenuItemChecker updateRefreshMenuItemChecker = new MenuItemUtils.UpdateRefreshMenuItemChecker() {
@ -283,35 +312,12 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterView.AdapterContextMenuInfo adapterInfo = (AdapterView.AdapterContextMenuInfo) menuInfo;
FeedItem item = (FeedItem) itemAccess.getItem(adapterInfo.position);
MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.feeditemlist_context, menu);
if (item != null) {
menu.setHeaderTitle(item.getTitle());
}
lastMenuInfo = (AdapterView.AdapterContextMenuInfo) menuInfo;
FeedItemMenuHandler.onPrepareMenu(menu, item);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterView.AdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
if (menuInfo == null) {
menuInfo = lastMenuInfo;
}
FeedItem selectedItem = feed.getItemAtIndex(menuInfo.position);
public boolean onContextItemSelected(@NonNull MenuItem item) {
FeedItem selectedItem = adapter.getSelectedItem();
if (selectedItem == null) {
Log.i(TAG, "Selected item at position " + menuInfo.position + " was null, ignoring selection");
Log.i(TAG, "Selected item at current position was null, ignoring selection");
return super.onContextItemSelected(item);
}
return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem);
}
@ -336,14 +342,19 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(FeedItemEvent event) {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
if (feed == null || feed.getItems() == null || adapter == null) {
if (feed == null || feed.getItems() == null) {
return;
} else if (adapter == null) {
loadItems();
return;
}
for (FeedItem item : event.items) {
for (int i = 0, size = event.items.size(); i < size; i++) {
FeedItem item = event.items.get(i);
int pos = FeedItemUtil.indexOfItemWithId(feed.getItems(), item.getId());
if (pos >= 0) {
loadItems();
return;
feed.getItems().remove(pos);
feed.getItems().add(pos, item);
adapter.notifyItemChanged(pos);
}
}
}
@ -356,14 +367,25 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
updateProgressBarVisibility();
}
if (adapter != null && update.mediaIds.length > 0) {
adapter.notifyDataSetChanged();
for (long mediaId : update.mediaIds) {
int pos = FeedItemUtil.indexOfItemWithMediaId(feed.getItems(), mediaId);
if (pos >= 0) {
adapter.notifyItemChanged(pos);
}
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(PlaybackPositionEvent event) {
if (adapter != null) {
adapter.notifyCurrentlyPlayingItemChanged(event, listView);
for (int i = 0; i < adapter.getItemCount(); i++) {
EpisodeItemViewHolder holder = (EpisodeItemViewHolder) recyclerView.findViewHolderForAdapterPosition(i);
if (holder != null && holder.isCurrentlyPlayingItem()) {
holder.notifyPlaybackPositionUpdated(event);
break;
}
}
}
}
@ -394,9 +416,10 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
if (isUpdatingFeed != updateRefreshMenuItemChecker.isRefreshing()) {
getActivity().supportInvalidateOptionsMenu();
}
if (listFooter != null) {
listFooter.setLoadingState(DownloadRequester.getInstance().isDownloadingFeeds());
if (!DownloadRequester.getInstance().isDownloadingFeeds()) {
nextPageLoader.getRoot().setVisibility(View.GONE);
}
nextPageLoader.setLoadingState(DownloadRequester.getInstance().isDownloadingFeeds());
}
private void displayList() {
@ -405,24 +428,20 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
return;
}
if (adapter == null) {
listView.setAdapter(null);
setupFooterView();
adapter = new FeedItemlistAdapter((MainActivity) getActivity(), itemAccess, false, true);
listView.setAdapter(adapter);
recyclerView.setAdapter(null);
adapter = new FeedItemListAdapter((MainActivity) getActivity());
recyclerView.setAdapter(adapter);
}
listView.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
adapter.notifyDataSetChanged();
adapter.updateItems(feed.getItems());
getActivity().supportInvalidateOptionsMenu();
if (feed != null && feed.getNextPageLink() == null && listFooter != null) {
listView.removeFooterView(listFooter.getRoot());
}
updateProgressBarVisibility();
}
private void refreshHeaderView() {
if (listView == null || feed == null || !headerCreated) {
if (recyclerView == null || feed == null || !headerCreated) {
Log.e(TAG, "Unable to refresh header view");
return;
}
@ -453,11 +472,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
}
private void setupHeaderView() {
if (listView == null || feed == null) {
Log.e(TAG, "Unable to setup listview: recyclerView = null or feed = null");
return;
}
if (headerCreated) {
if (feed == null || headerCreated) {
return;
}
@ -504,49 +519,6 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
.into(imgvCover);
}
private void setupFooterView() {
if (listView == null || feed == null) {
Log.e(TAG, "Unable to setup listview: recyclerView = null or feed = null");
return;
}
if (feed.isPaged() && feed.getNextPageLink() != null) {
LayoutInflater inflater = (LayoutInflater)
getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View header = inflater.inflate(R.layout.more_content_list_footer, listView, false);
listView.addFooterView(header);
listFooter = new MoreContentListFooterUtil(header);
listFooter.setClickListener(() -> {
if (feed != null) {
try {
DBTasks.loadNextPageOfFeed(getActivity(), feed, false);
} catch (DownloadRequestException e) {
e.printStackTrace();
DownloadRequestErrorDialogCreator.newRequestErrorDialog(getActivity(), e.getMessage());
}
}
});
}
}
private final FeedItemlistAdapter.ItemAccess itemAccess = new FeedItemlistAdapter.ItemAccess() {
@Override
public FeedItem getItem(int position) {
if (feed != null && 0 <= position && position < feed.getNumOfItems()) {
return feed.getItemAtIndex(position);
} else {
return null;
}
}
@Override
public int getCount() {
return (feed != null) ? feed.getNumOfItems() : 0;
}
};
private void loadItems() {
if (disposable != null) {
disposable.dispose();
@ -576,4 +548,18 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
}
return Optional.ofNullable(feed);
}
private static class FeedItemListAdapter extends EpisodeItemListAdapter {
public FeedItemListAdapter(MainActivity mainActivity) {
super(mainActivity);
}
@NonNull
@Override
public EpisodeItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
EpisodeItemViewHolder viewHolder = super.onCreateViewHolder(parent, viewType);
viewHolder.coverHolder.setVisibility(View.GONE);
return viewHolder;
}
}
}

View File

@ -12,7 +12,6 @@ import android.view.ViewGroup;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.AllEpisodesRecycleAdapter;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
@ -53,7 +52,8 @@ public class NewEpisodesFragment extends EpisodesListFragment {
ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(0,
ItemTouchHelper.RIGHT) {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
return false;
}
@ -62,33 +62,6 @@ public class NewEpisodesFragment extends EpisodesListFragment {
EpisodeItemViewHolder holder = (EpisodeItemViewHolder) viewHolder;
FeedItemMenuHandler.removeNewFlagWithUndo(NewEpisodesFragment.this, holder.getFeedItem());
}
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder,
int actionState) {
// We only want the active item
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
if (viewHolder instanceof AllEpisodesRecycleAdapter.ItemTouchHelperViewHolder) {
AllEpisodesRecycleAdapter.ItemTouchHelperViewHolder itemViewHolder =
(AllEpisodesRecycleAdapter.ItemTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemSelected();
}
}
super.onSelectedChanged(viewHolder, actionState);
}
@Override
public void clearView(RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
if (viewHolder instanceof AllEpisodesRecycleAdapter.ItemTouchHelperViewHolder) {
AllEpisodesRecycleAdapter.ItemTouchHelperViewHolder itemViewHolder =
(AllEpisodesRecycleAdapter.ItemTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemClear();
}
}
};
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(simpleItemTouchCallback);

View File

@ -9,26 +9,31 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.MenuItemCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.FeedItemlistAdapter;
import de.danoeh.antennapod.adapter.EpisodeItemListAdapter;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.DownloaderUpdate;
import de.danoeh.antennapod.core.event.FeedItemEvent;
import de.danoeh.antennapod.core.event.PlaybackHistoryEvent;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.event.PlayerStatusEvent;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.view.EmptyViewHandler;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
@ -39,13 +44,13 @@ import org.greenrobot.eventbus.ThreadMode;
import java.util.List;
public class PlaybackHistoryFragment extends Fragment implements AdapterView.OnItemClickListener {
public class PlaybackHistoryFragment extends Fragment {
public static final String TAG = "PlaybackHistoryFragment";
private List<FeedItem> playbackHistory;
private FeedItemlistAdapter adapter;
private PlaybackHistoryListAdapter adapter;
private Disposable disposable;
private ListView listView;
private RecyclerView recyclerView;
@Override
public void onCreate(Bundle savedInstanceState) {
@ -62,18 +67,20 @@ public class PlaybackHistoryFragment extends Fragment implements AdapterView.OnI
toolbar.setTitle(R.string.playback_history_label);
((AppCompatActivity) getActivity()).setSupportActionBar(toolbar);
listView = root.findViewById(android.R.id.list);
recyclerView = root.findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
recyclerView.setHasFixedSize(true);
recyclerView.addItemDecoration(new HorizontalDividerItemDecoration.Builder(getActivity()).build());
recyclerView.setVisibility(View.GONE);
adapter = new PlaybackHistoryListAdapter((MainActivity) getActivity());
recyclerView.setAdapter(adapter);
EmptyViewHandler emptyView = new EmptyViewHandler(getActivity());
emptyView.setIcon(R.attr.ic_history);
emptyView.setTitle(R.string.no_history_head_label);
emptyView.setMessage(R.string.no_history_label);
emptyView.attachToListView(listView);
// played items shoudln't be transparent for this fragment since, *all* items
// in this fragment will, by definition, be played. So it serves no purpose and can make
// it harder to read.
adapter = new FeedItemlistAdapter((MainActivity) getActivity(), itemAccess, true, false);
listView.setAdapter(adapter);
emptyView.attachToRecyclerView(recyclerView);
return root;
}
@ -93,17 +100,51 @@ public class PlaybackHistoryFragment extends Fragment implements AdapterView.OnI
}
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEvent(DownloadEvent event) {
Log.d(TAG, "onEvent() called with: " + "event = [" + event + "]");
adapter.notifyDataSetChanged();
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(FeedItemEvent event) {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
if (playbackHistory == null) {
return;
} else if (adapter == null) {
loadItems();
return;
}
for (int i = 0, size = event.items.size(); i < size; i++) {
FeedItem item = event.items.get(i);
int pos = FeedItemUtil.indexOfItemWithId(playbackHistory, item.getId());
if (pos >= 0) {
playbackHistory.remove(pos);
playbackHistory.add(pos, item);
adapter.notifyItemChanged(pos);
}
}
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
position -= listView.getHeaderViewsCount();
long[] ids = FeedItemUtil.getIds(playbackHistory);
((MainActivity) getActivity()).loadChildFragment(ItemPagerFragment.newInstance(ids, position));
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(DownloadEvent event) {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
DownloaderUpdate update = event.update;
if (adapter != null && update.mediaIds.length > 0) {
for (long mediaId : update.mediaIds) {
int pos = FeedItemUtil.indexOfItemWithMediaId(playbackHistory, mediaId);
if (pos >= 0) {
adapter.notifyItemChanged(pos);
}
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(PlaybackPositionEvent event) {
if (adapter != null) {
for (int i = 0; i < adapter.getItemCount(); i++) {
EpisodeItemViewHolder holder = (EpisodeItemViewHolder) recyclerView.findViewHolderForAdapterPosition(i);
if (holder != null && holder.isCurrentlyPlayingItem()) {
holder.notifyPlaybackPositionUpdated(event);
break;
}
}
}
}
@Override
@ -143,19 +184,14 @@ public class PlaybackHistoryFragment extends Fragment implements AdapterView.OnI
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(FeedItemEvent event) {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
if (playbackHistory == null) {
return;
}
for (FeedItem item : event.items) {
int pos = FeedItemUtil.indexOfItemWithId(playbackHistory, item.getId());
if (pos >= 0) {
loadItems();
return;
}
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
FeedItem selectedItem = adapter.getSelectedItem();
if (selectedItem == null) {
Log.i(TAG, "Selected item at current position was null, ignoring selection");
return super.onContextItemSelected(item);
}
return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem);
}
@Subscribe(threadMode = ThreadMode.MAIN)
@ -175,23 +211,6 @@ public class PlaybackHistoryFragment extends Fragment implements AdapterView.OnI
getActivity().supportInvalidateOptionsMenu();
}
private final FeedItemlistAdapter.ItemAccess itemAccess = new FeedItemlistAdapter.ItemAccess() {
@Override
public int getCount() {
return (playbackHistory != null) ? playbackHistory.size() : 0;
}
@Override
public FeedItem getItem(int position) {
if (playbackHistory != null && 0 <= position && position < playbackHistory.size()) {
return playbackHistory.get(position);
} else {
return null;
}
}
};
private void loadItems() {
if (disposable != null) {
disposable.dispose();
@ -202,6 +221,7 @@ public class PlaybackHistoryFragment extends Fragment implements AdapterView.OnI
.subscribe(result -> {
if (result != null) {
playbackHistory = result;
adapter.updateItems(playbackHistory);
onFragmentLoaded();
}
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
@ -213,4 +233,20 @@ public class PlaybackHistoryFragment extends Fragment implements AdapterView.OnI
DBReader.loadAdditionalFeedItemListData(history);
return history;
}
private static class PlaybackHistoryListAdapter extends EpisodeItemListAdapter {
public PlaybackHistoryListAdapter(MainActivity mainActivity) {
super(mainActivity);
}
@Override
public void onBindViewHolder(EpisodeItemViewHolder holder, int pos) {
super.onBindViewHolder(holder, pos);
// played items shouldn't be transparent for this fragment since, *all* items
// in this fragment will, by definition, be played. So it serves no purpose and can make
// it harder to read.
holder.itemView.setAlpha(1.0f);
}
}
}

View File

@ -14,27 +14,15 @@ import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SearchView;
import androidx.core.view.MenuItemCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.SimpleItemAnimator;
import com.google.android.material.snackbar.Snackbar;
import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.QueueRecyclerAdapter;
@ -44,8 +32,8 @@ import de.danoeh.antennapod.core.event.DownloaderUpdate;
import de.danoeh.antennapod.core.event.FeedItemEvent;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.event.PlayerStatusEvent;
import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.event.QueueEvent;
import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.preferences.UserPreferences;
@ -55,24 +43,29 @@ import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.SortOrder;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.view.EmptyViewHandler;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.List;
import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_DELETE;
import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_REMOVE_FROM_QUEUE;
import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_DOWNLOAD;
import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_REMOVE_FROM_QUEUE;
/**
* Shows all items in the queue
* Shows all items in the queue.
*/
public class QueueFragment extends Fragment {
public static final String TAG = "QueueFragment";
@ -179,10 +172,10 @@ public class QueueFragment extends Fragment {
loadItems(true);
return;
}
for(int i=0, size = event.items.size(); i < size; i++) {
for (int i = 0, size = event.items.size(); i < size; i++) {
FeedItem item = event.items.get(i);
int pos = FeedItemUtil.indexOfItemWithId(queue, item.getId());
if(pos >= 0) {
if (pos >= 0) {
queue.remove(pos);
queue.add(pos, item);
recyclerAdapter.notifyItemChanged(pos);
@ -501,12 +494,13 @@ public class QueueFragment extends Fragment {
int dragTo = -1;
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
int fromPosition = viewHolder.getAdapterPosition();
int toPosition = target.getAdapterPosition();
// Update tracked position
if(dragFrom == -1) {
if (dragFrom == -1) {
dragFrom = fromPosition;
}
dragTo = toPosition;
@ -514,7 +508,7 @@ public class QueueFragment extends Fragment {
int from = viewHolder.getAdapterPosition();
int to = target.getAdapterPosition();
Log.d(TAG, "move(" + from + ", " + to + ") in memory");
if(from >= queue.size() || to >= queue.size()) {
if (from >= queue.size() || to >= queue.size()) {
return false;
}
queue.add(to, queue.remove(from));
@ -524,7 +518,7 @@ public class QueueFragment extends Fragment {
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
if(disposable != null) {
if (disposable != null) {
disposable.dispose();
}
final int position = viewHolder.getAdapterPosition();
@ -556,36 +550,14 @@ public class QueueFragment extends Fragment {
}
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder,
int actionState) {
// We only want the active item
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
if (viewHolder instanceof QueueRecyclerAdapter.ItemTouchHelperViewHolder) {
QueueRecyclerAdapter.ItemTouchHelperViewHolder itemViewHolder =
(QueueRecyclerAdapter.ItemTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemSelected();
}
}
super.onSelectedChanged(viewHolder, actionState);
}
@Override
public void clearView(RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder) {
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
// Check if drag finished
if(dragFrom != -1 && dragTo != -1 && dragFrom != dragTo) {
if (dragFrom != -1 && dragTo != -1 && dragFrom != dragTo) {
reallyMoved(dragFrom, dragTo);
}
dragFrom = dragTo = -1;
if (viewHolder instanceof QueueRecyclerAdapter.ItemTouchHelperViewHolder) {
QueueRecyclerAdapter.ItemTouchHelperViewHolder itemViewHolder =
(QueueRecyclerAdapter.ItemTouchHelperViewHolder) viewHolder;
itemViewHolder.onItemClear();
}
}
private void reallyMoved(int from, int to) {
@ -613,11 +585,11 @@ public class QueueFragment extends Fragment {
if (queue != null && queue.size() > 0) {
if (recyclerAdapter == null) {
MainActivity activity = (MainActivity) getActivity();
recyclerAdapter = new QueueRecyclerAdapter(activity, itemAccess, itemTouchHelper);
recyclerAdapter.setHasStableIds(true);
recyclerAdapter = new QueueRecyclerAdapter(activity, itemTouchHelper);
recyclerView.setAdapter(recyclerAdapter);
emptyView.updateAdapter(recyclerAdapter);
}
recyclerAdapter.updateItems(queue);
recyclerView.setVisibility(View.VISIBLE);
} else {
recyclerAdapter = null;
@ -657,29 +629,9 @@ public class QueueFragment extends Fragment {
infoBar.setText(info);
}
private final QueueRecyclerAdapter.ItemAccess itemAccess = new QueueRecyclerAdapter.ItemAccess() {
@Override
public int getCount() {
return queue != null ? queue.size() : 0;
}
@Override
public FeedItem getItem(int position) {
if (queue != null && 0 <= position && position < queue.size()) {
return queue.get(position);
}
return null;
}
@Override
public LongList getQueueIds() {
return queue != null ? LongList.of(FeedItemUtil.getIds(queue)) : new LongList(0);
}
};
private void loadItems(final boolean restoreScrollPosition) {
Log.d(TAG, "loadItems()");
if(disposable != null) {
if (disposable != null) {
disposable.dispose();
}
if (queue == null) {
@ -694,7 +646,7 @@ public class QueueFragment extends Fragment {
progLoading.setVisibility(View.GONE);
queue = items;
onFragmentLoaded(restoreScrollPosition);
if(recyclerAdapter != null) {
if (recyclerAdapter != null) {
recyclerAdapter.notifyDataSetChanged();
}
}, error -> Log.e(TAG, Log.getStackTraceString(error)));

View File

@ -3,55 +3,65 @@ package de.danoeh.antennapod.fragment;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SearchView;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.FeedItemlistAdapter;
import de.danoeh.antennapod.adapter.EpisodeItemListAdapter;
import de.danoeh.antennapod.adapter.FeedSearchResultAdapter;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.DownloaderUpdate;
import de.danoeh.antennapod.core.event.FeedItemEvent;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.event.PlayerStatusEvent;
import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedComponent;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.storage.FeedSearcher;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.view.EmptyViewHandler;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.List;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.List;
/**
* Performs a search operation on all feeds or one specific feed and displays the search result.
*/
public class SearchFragment extends Fragment implements AdapterView.OnItemClickListener {
public class SearchFragment extends Fragment {
private static final String TAG = "SearchFragment";
private static final String ARG_QUERY = "query";
private static final String ARG_FEED = "feed";
private FeedItemlistAdapter searchAdapter;
private List<FeedComponent> searchResults = new ArrayList<>();
private EpisodeItemListAdapter adapter;
private FeedSearchResultAdapter adapterFeeds;
private Disposable disposable;
private ProgressBar progressBar;
private EmptyViewHandler emptyViewHandler;
private RecyclerView recyclerView;
private RecyclerView recyclerViewFeeds;
private List<FeedItem> results;
/**
* Create a new SearchFragment that searches all feeds.
@ -104,14 +114,27 @@ public class SearchFragment extends Fragment implements AdapterView.OnItemClickL
@Nullable Bundle savedInstanceState) {
View layout = inflater.inflate(R.layout.search_fragment, container, false);
((AppCompatActivity) getActivity()).setSupportActionBar(layout.findViewById(R.id.toolbar));
ListView listView = layout.findViewById(R.id.listview);
progressBar = layout.findViewById(R.id.progressBar);
searchAdapter = new FeedItemlistAdapter((MainActivity) getActivity(), itemAccess, true, true);
listView.setAdapter(searchAdapter);
listView.setOnItemClickListener(this);
recyclerView = layout.findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
recyclerView.setHasFixedSize(true);
recyclerView.addItemDecoration(new HorizontalDividerItemDecoration.Builder(getActivity()).build());
recyclerView.setVisibility(View.GONE);
adapter = new EpisodeItemListAdapter((MainActivity) getActivity());
recyclerView.setAdapter(adapter);
recyclerViewFeeds = layout.findViewById(R.id.recyclerViewFeeds);
LinearLayoutManager layoutManagerFeeds = new LinearLayoutManager(getActivity());
layoutManagerFeeds.setOrientation(RecyclerView.HORIZONTAL);
recyclerViewFeeds.setLayoutManager(layoutManagerFeeds);
recyclerViewFeeds.setHasFixedSize(true);
adapterFeeds = new FeedSearchResultAdapter((MainActivity) getActivity());
recyclerViewFeeds.setAdapter(adapterFeeds);
emptyViewHandler = new EmptyViewHandler(getContext());
emptyViewHandler.attachToListView(listView);
emptyViewHandler.attachToRecyclerView(recyclerView);
emptyViewHandler.setIcon(R.attr.action_search);
emptyViewHandler.setTitle(R.string.search_status_no_results);
EventBus.getDefault().register(this);
@ -124,17 +147,6 @@ public class SearchFragment extends Fragment implements AdapterView.OnItemClickL
EventBus.getDefault().unregister(this);
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
FeedComponent comp = searchAdapter.getItem(position);
if (comp.getClass() == Feed.class) {
((MainActivity) getActivity()).loadFeedFragmentById(comp.getId(), null);
} else if (comp.getClass() == FeedItem.class) {
FeedItem item = (FeedItem) comp;
((MainActivity) getActivity()).loadChildFragment(ItemPagerFragment.newInstance(item.getId()));
}
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
@ -173,42 +185,72 @@ public class SearchFragment extends Fragment implements AdapterView.OnItemClickL
});
}
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
FeedItem selectedItem = adapter.getSelectedItem();
if (selectedItem == null) {
Log.i(TAG, "Selected item at current position was null, ignoring selection");
return super.onContextItemSelected(item);
}
return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem);
}
@Subscribe
public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) {
search();
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(FeedItemEvent event) {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
if (results == null) {
return;
} else if (adapter == null) {
search();
return;
}
for (int i = 0, size = event.items.size(); i < size; i++) {
FeedItem item = event.items.get(i);
int pos = FeedItemUtil.indexOfItemWithId(results, item.getId());
if (pos >= 0) {
results.remove(pos);
results.add(pos, item);
adapter.notifyItemChanged(pos);
}
}
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(DownloadEvent event) {
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
if (searchAdapter != null) {
searchAdapter.notifyDataSetChanged();
}
}
private void onSearchResults(List<FeedComponent> results) {
progressBar.setVisibility(View.GONE);
searchResults = results;
searchAdapter.notifyDataSetChanged();
String query = getArguments().getString(ARG_QUERY);
emptyViewHandler.setMessage(getString(R.string.no_results_for_query, query));
}
private final FeedItemlistAdapter.ItemAccess itemAccess = new FeedItemlistAdapter.ItemAccess() {
@Override
public int getCount() {
return searchResults.size();
}
@Override
public FeedComponent getItem(int position) {
if (0 <= position && position < searchResults.size()) {
return searchResults.get(position);
} else {
return null;
DownloaderUpdate update = event.update;
if (adapter != null && update.mediaIds.length > 0) {
for (long mediaId : update.mediaIds) {
int pos = FeedItemUtil.indexOfItemWithMediaId(results, mediaId);
if (pos >= 0) {
adapter.notifyItemChanged(pos);
}
}
}
};
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(PlaybackPositionEvent event) {
if (adapter != null) {
for (int i = 0; i < adapter.getItemCount(); i++) {
EpisodeItemViewHolder holder = (EpisodeItemViewHolder) recyclerView.findViewHolderForAdapterPosition(i);
if (holder != null && holder.isCurrentlyPlayingItem()) {
holder.notifyPlaybackPositionUpdated(event);
break;
}
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onPlayerStatusChanged(PlayerStatusEvent event) {
search();
}
private void search() {
if (disposable != null) {
@ -219,15 +261,22 @@ public class SearchFragment extends Fragment implements AdapterView.OnItemClickL
disposable = Observable.fromCallable(this::performSearch)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onSearchResults, error -> Log.e(TAG, Log.getStackTraceString(error)));
.subscribe(results -> {
progressBar.setVisibility(View.GONE);
this.results = results.first;
adapter.updateItems(results.first);
adapterFeeds.updateData(results.second);
String query = getArguments().getString(ARG_QUERY);
emptyViewHandler.setMessage(getString(R.string.no_results_for_query, query));
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
}
@NonNull
private List<FeedComponent> performSearch() {
Bundle args = getArguments();
String query = args.getString(ARG_QUERY);
long feed = args.getLong(ARG_FEED);
Context context = getActivity();
return FeedSearcher.performSearch(context, query, feed);
private Pair<List<FeedItem>, List<Feed>> performSearch() {
String query = getArguments().getString(ARG_QUERY);
long feed = getArguments().getLong(ARG_FEED);
List<FeedItem> items = FeedSearcher.searchFeedItems(getContext(), query, feed);
List<Feed> feeds = FeedSearcher.searchFeeds(getContext(), query);
return new Pair<>(items, feeds);
}
}

View File

@ -78,8 +78,13 @@ public class CircularProgressBar extends View {
}
if (Math.abs(percentage - targetPercentage) > EPSILON) {
float delta = Math.min(0.02f, Math.abs(targetPercentage - percentage));
percentage += delta * ((targetPercentage - percentage) > 0 ? 1f : -1f);
float speed = 0.02f;
if (Math.abs(targetPercentage - percentage) < 0.1 && targetPercentage > percentage) {
speed = 0.006f;
}
float delta = Math.min(speed, Math.abs(targetPercentage - percentage));
float direction = ((targetPercentage - percentage) > 0 ? 1f : -1f);
percentage += delta * direction;
invalidate();
}
}

View File

@ -1,74 +0,0 @@
package de.danoeh.antennapod.view;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.ListView;
import androidx.core.view.NestedScrollingChild;
import androidx.core.view.NestedScrollingChildHelper;
/**
* ListView that can be wrapped in NestedScrollView
* Based on https://stackoverflow.com/a/34920961.
*/
public class NestedScrollingListView extends ListView implements NestedScrollingChild {
private final NestedScrollingChildHelper nestedScrollingChildHelper;
public NestedScrollingListView(Context context) {
super(context);
nestedScrollingChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
public NestedScrollingListView(Context context, AttributeSet attrs) {
super(context, attrs);
nestedScrollingChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
@Override
public void setNestedScrollingEnabled(boolean enabled) {
nestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return nestedScrollingChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return nestedScrollingChildHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
nestedScrollingChildHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return nestedScrollingChildHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return nestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return nestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return nestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return nestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
}

View File

@ -10,7 +10,11 @@ import de.danoeh.antennapod.core.R;
* From http://stackoverflow.com/a/19449488/6839
*/
public class SquareImageView extends AppCompatImageView {
private boolean useMinimum = false;
public static final int DIRECTION_WIDTH = 0;
public static final int DIRECTION_HEIGHT = 1;
public static final int DIRECTION_MINIMUM = 2;
private int direction = DIRECTION_WIDTH;
public SquareImageView(Context context) {
super(context);
@ -27,20 +31,32 @@ public class SquareImageView extends AppCompatImageView {
}
private void loadAttrs(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, new int[]{R.styleable.SquareImageView_useMinimum});
useMinimum = a.getBoolean(0, false);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SquareImageView);
direction = a.getInt(R.styleable.SquareImageView_direction, DIRECTION_WIDTH);
a.recycle();
}
public void setDirection(int direction) {
this.direction = direction;
requestLayout();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int size = getMeasuredWidth();
if (useMinimum) {
size = Math.min(getMeasuredWidth(), getMeasuredHeight());
switch (direction) {
case DIRECTION_MINIMUM:
int size = Math.min(getMeasuredWidth(), getMeasuredHeight());
setMeasuredDimension(size, size);
break;
case DIRECTION_HEIGHT:
setMeasuredDimension(getMeasuredHeight(), getMeasuredHeight());
break;
default:
setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth());
break;
}
setMeasuredDimension(size, size);
}
}

View File

@ -1,16 +1,13 @@
package de.danoeh.antennapod.view.viewholder;
import android.graphics.Color;
import android.os.Build;
import android.text.Layout;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.cardview.widget.CardView;
import androidx.recyclerview.widget.RecyclerView;
@ -36,8 +33,7 @@ import de.danoeh.antennapod.view.CircularProgressBar;
/**
* Holds the view which shows FeedItems.
*/
public class EpisodeItemViewHolder extends FeedComponentViewHolder
implements QueueRecyclerAdapter.ItemTouchHelperViewHolder {
public class EpisodeItemViewHolder extends RecyclerView.ViewHolder {
private static final String TAG = "EpisodeItemViewHolder";
private final View container;
@ -91,16 +87,6 @@ public class EpisodeItemViewHolder extends FeedComponentViewHolder
itemView.setTag(this);
}
@Override
public void onItemSelected() {
itemView.setAlpha(0.5f);
}
@Override
public void onItemClear() {
itemView.setAlpha(1.0f);
}
public void bind(FeedItem item) {
this.item = item;
placeholder.setText(item.getFeed().getTitle());

View File

@ -1,15 +0,0 @@
package de.danoeh.antennapod.view.viewholder;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
/**
* Holds the view which shows FeedComponents.
*/
public class FeedComponentViewHolder extends RecyclerView.ViewHolder {
public FeedComponentViewHolder(@NonNull View itemView) {
super(itemView);
}
}

View File

@ -1,62 +0,0 @@
package de.danoeh.antennapod.view.viewholder;
import android.os.Build;
import android.text.Layout;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.cardview.widget.CardView;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.CoverLoader;
import de.danoeh.antennapod.core.feed.Feed;
/**
* Holds the view which shows feeds.
*/
public class FeedViewHolder extends FeedComponentViewHolder {
private static final String TAG = "FeedViewHolder";
private final TextView placeholder;
private final ImageView cover;
private final TextView title;
public final CardView coverHolder;
private final MainActivity activity;
private Feed feed;
public FeedViewHolder(MainActivity activity, ViewGroup parent) {
super(LayoutInflater.from(activity).inflate(R.layout.feeditemlist_item, parent, false));
this.activity = activity;
placeholder = itemView.findViewById(R.id.txtvPlaceholder);
cover = itemView.findViewById(R.id.imgvCover);
coverHolder = itemView.findViewById(R.id.coverHolder);
title = itemView.findViewById(R.id.txtvTitle);
if (Build.VERSION.SDK_INT >= 23) {
title.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL);
}
itemView.findViewById(R.id.secondaryActionButton).setVisibility(View.GONE);
itemView.findViewById(R.id.status).setVisibility(View.GONE);
itemView.findViewById(R.id.progress).setVisibility(View.GONE);
itemView.findViewById(R.id.drag_handle).setVisibility(View.GONE);
itemView.setTag(this);
}
public void bind(Feed feed) {
this.feed = feed;
placeholder.setText(feed.getTitle());
title.setText(feed.getTitle());
if (coverHolder.getVisibility() == View.VISIBLE) {
new CoverLoader(activity)
.withUri(feed.getImageLocation())
.withPlaceholderView(placeholder)
.withCoverView(cover)
.load();
}
}
}

View File

@ -2,7 +2,8 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="match_parent" android:layout_width="match_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
@ -11,6 +12,7 @@
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:title="@string/add_feed_label"
app:navigationIcon="?homeAsUpIndicator"
android:id="@+id/toolbar"/>
<androidx.cardview.widget.CardView
@ -19,8 +21,7 @@
app:cardCornerRadius="4dp"
app:cardElevation="4dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp">
android:layout_marginRight="16dp">
<LinearLayout
android:layout_width="match_parent"

View File

@ -20,7 +20,7 @@
android:transitionName="coverTransition"
tools:src="@android:drawable/sym_def_app_icon"
android:foreground="?attr/selectableItemBackgroundBorderless"
squareImageView:useMinimum="true" />
squareImageView:direction="minimum" />
<TextView
android:id="@+id/txtvPodcastTitle"

View File

@ -39,17 +39,17 @@
android:theme="?attr/actionBarTheme"
android:layout_alignParentTop="true"
android:id="@+id/toolbar"
app:navigationIcon="?homeAsUpIndicator"
app:layout_collapseMode="pin"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<de.danoeh.antennapod.view.NestedScrollingListView
android:clipToPadding="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@android:id/list" />
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<ProgressBar
android:id="@+id/progLoading"
@ -59,4 +59,11 @@
android:indeterminateOnly="true"
android:visibility="gone"/>
<include
layout="@layout/more_content_list_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:visibility="gone"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -25,6 +25,7 @@
android:src="?attr/dragview_background"
android:paddingEnd="8dp"
android:paddingRight="8dp"
android:visibility="gone"
tools:src="@drawable/ic_drag_vertical_grey600_48dp"
tools:background="@android:color/holo_green_dark"/>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:squareImageView="http://schemas.android.com/apk/de.danoeh.antennapod"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="4dp"
@ -12,7 +13,8 @@
android:layout_height="match_parent"
android:elevation="4dp"
android:outlineProvider="bounds"
android:foreground="?android:attr/selectableItemBackground"/>
android:foreground="?android:attr/selectableItemBackground"
squareImageView:direction="width" />
</LinearLayout>

View File

@ -22,9 +22,21 @@
android:layout_height="wrap_content"
android:layout_gravity="center"/>
<ListView
<androidx.recyclerview.widget.RecyclerView
android:layout_below="@id/toolbar"
android:id="@+id/listview"
android:id="@+id/recyclerViewFeeds"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:clipToPadding="false"/>
<androidx.recyclerview.widget.RecyclerView
android:layout_below="@id/recyclerViewFeeds"
android:id="@+id/recyclerView"
android:layout_marginTop="-4dp"
android:paddingTop="12dp"
android:clipToPadding="false"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:squareImageView="http://schemas.android.com/apk/de.danoeh.antennapod"
android:layout_width="match_parent"
android:layout_height="96dp"
android:padding="4dp"
android:clipToPadding="false">
<de.danoeh.antennapod.view.SquareImageView
android:id="@+id/discovery_cover"
android:layout_width="match_parent"
android:layout_height="96dp"
android:elevation="4dp"
android:outlineProvider="bounds"
android:foreground="?android:attr/selectableItemBackground"
squareImageView:direction="height" />
</LinearLayout>

View File

@ -11,11 +11,12 @@
android:layout_alignParentTop="true"
android:id="@+id/toolbar"/>
<ListView android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/toolbar"
android:id="@android:id/list"
android:clipToPadding="false"/>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/toolbar"
android:id="@+id/recyclerView"
android:clipToPadding="false"/>
<ProgressBar
android:id="@+id/progLoading"

View File

@ -1,7 +1,9 @@
<?xml version='1.0' encoding='utf-8'?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout
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"
xmlns:squareImageView="http://schemas.android.com/apk/de.danoeh.antennapod"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -10,7 +12,8 @@
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scaleType="centerCrop"
tools:src="@mipmap/ic_launcher_round" />
tools:src="@mipmap/ic_launcher_round"
squareImageView:direction="width"/>
<com.joanzapata.iconify.widget.IconTextView
android:id="@+id/txtvTitle"

View File

@ -54,7 +54,6 @@ import de.danoeh.antennapod.core.event.settings.SpeedPresetChangedEvent;
import de.danoeh.antennapod.core.event.settings.VolumeAdaptionChangedEvent;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedComponent;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
@ -1649,18 +1648,11 @@ public class PlaybackService extends MediaBrowserServiceCompat {
public void onPlayFromSearch(String query, Bundle extras) {
Log.d(TAG, "onPlayFromSearch query=" + query + " extras=" + extras.toString());
List<FeedComponent> results = FeedSearcher.performSearch(getBaseContext(), query, 0);
for (FeedComponent result : results) {
if (result instanceof FeedItem) {
try {
FeedMedia media = ((FeedItem) result).getMedia();
mediaPlayer.playMediaObject(media, !media.localFileAvailable(), true, true);
return;
} catch (Exception e) {
Log.d(TAG, e.getMessage());
e.printStackTrace();
}
}
List<FeedItem> results = FeedSearcher.searchFeedItems(getBaseContext(), query, 0);
if (results.size() > 0 && results.get(0).getMedia() != null) {
FeedMedia media = results.get(0).getMedia();
mediaPlayer.playMediaObject(media, !media.localFileAvailable(), true, true);
return;
}
onPlay();
}

View File

@ -126,8 +126,6 @@ public class DBWriter {
}
}
EventBus.getDefault().post(FeedItemEvent.deletedMedia(Collections.singletonList(media.getItem())));
EventBus.getDefault().post(new UnreadItemsUpdateEvent());
return true;
}

View File

@ -7,6 +7,7 @@ import de.danoeh.antennapod.core.feed.FeedComponent;
import de.danoeh.antennapod.core.feed.FeedItem;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
@ -19,32 +20,25 @@ public class FeedSearcher {
}
/**
* Search through a feed, or all feeds, for episodes that match the query in either the title,
* chapter, or show notes. The search is first performed on titles, then chapters, and finally
* show notes. The list of resulting episodes also describes where the first match occurred
* (title, chapters, or show notes).
*
* @param context Used for database access
* @param query search query
* @param selectedFeed feed to search, 0 to search through all feeds
* @return list of episodes containing the query
*/
@NonNull
public static List<FeedComponent> performSearch(final Context context, final String query, final long selectedFeed) {
final List<FeedComponent> result = new ArrayList<>();
public static List<FeedItem> searchFeedItems(final Context context, final String query, final long selectedFeed) {
try {
FutureTask<List<FeedItem>> itemSearchTask = DBTasks.searchFeedItems(context, selectedFeed, query);
itemSearchTask.run();
if (selectedFeed == 0) {
FutureTask<List<Feed>> feedSearchTask = DBTasks.searchFeeds(context, query);
feedSearchTask.run();
result.addAll(feedSearchTask.get());
}
result.addAll(itemSearchTask.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
return itemSearchTask.get();
} catch (ExecutionException | InterruptedException e) {
return Collections.emptyList();
}
}
@NonNull
public static List<Feed> searchFeeds(final Context context, final String query) {
try {
FutureTask<List<Feed>> feedSearchTask = DBTasks.searchFeeds(context, query);
feedSearchTask.run();
return feedSearchTask.get();
} catch (ExecutionException | InterruptedException e) {
return Collections.emptyList();
}
return result;
}
}

View File

@ -1,28 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main"
android:id="@+id/more_content_list_footer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground">
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:background="?android:attr/windowBackground"
android:gravity="center"
android:padding="8dp">
<ImageView
android:id="@+id/imgExpand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_gravity="center"
android:layout_margin="16dp"
android:contentDescription="@string/load_next_page_label"
app:srcCompat="?attr/ic_load_more" />
<ProgressBar
android:id="@+id/progBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="16dp"
android:layout_width="16dp"
android:layout_height="16dp"
android:indeterminateOnly="true"
android:visibility="gone" />
</FrameLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/load_next_page_label"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"/>
</LinearLayout>

View File

@ -57,6 +57,10 @@
<attr name="action_icon_color" format="color"/>
<declare-styleable name="SquareImageView">
<attr name="useMinimum" format="boolean" />
<attr name="direction" format="enum">
<enum name="width" value="0"/>
<enum name="height" value="1"/>
<enum name="minimum" value="2"/>
</attr>
</declare-styleable>
</resources>