From 56c1f501275f3c0e7d89a0a179ff34d49daa04e1 Mon Sep 17 00:00:00 2001 From: Krzysztof Grabowski Date: Sat, 4 Jun 2016 20:18:24 +0200 Subject: [PATCH] Bath remove favourite songs, improvements --- .../centrumfm/activity/MainActivity.java | 3 +- .../fragment/FavouriteSongsFragment.java | 99 ++++++++++++-- .../fragment/SongArchiveFragment.java | 4 +- .../rest/adapter/SelectableAdapter.java | 74 +++++++++++ .../centrumfm/rest/adapter/SongsAdapter.java | 34 +++-- .../centrumfm/util/database/AsyncWrapper.java | 11 ++ .../drawable-hdpi/ic_delete_white_24dp.png | Bin 0 -> 161 bytes .../drawable-mdpi/ic_delete_white_24dp.png | Bin 0 -> 115 bytes .../drawable-xhdpi/ic_delete_white_24dp.png | Bin 0 -> 151 bytes .../drawable-xxhdpi/ic_delete_white_24dp.png | Bin 0 -> 194 bytes .../drawable-xxxhdpi/ic_delete_white_24dp.png | Bin 0 -> 243 bytes app/src/main/res/layout/list_item_song.xml | 121 ++++++++++-------- app/src/main/res/layout/player_slider.xml | 4 +- .../main/res/menu/context_menu_fav_songs.xml | 2 + app/src/main/res/menu/context_menu_songs.xml | 2 + .../res/menu/menu_actionmode_selected.xml | 10 ++ app/src/main/res/menu/menu_drawer.xml | 3 + app/src/main/res/menu/menu_main.xml | 1 + app/src/main/res/menu/menu_songs.xml | 1 + app/src/main/res/values-v21/styles.xml | 2 + app/src/main/res/values/strings.xml | 7 + app/src/main/res/values/styles.xml | 3 +- 22 files changed, 304 insertions(+), 77 deletions(-) create mode 100644 app/src/main/java/org/indywidualni/centrumfm/rest/adapter/SelectableAdapter.java create mode 100644 app/src/main/res/drawable-hdpi/ic_delete_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_delete_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_delete_white_24dp.png create mode 100644 app/src/main/res/menu/menu_actionmode_selected.xml diff --git a/app/src/main/java/org/indywidualni/centrumfm/activity/MainActivity.java b/app/src/main/java/org/indywidualni/centrumfm/activity/MainActivity.java index cc31dbf..8885cc1 100644 --- a/app/src/main/java/org/indywidualni/centrumfm/activity/MainActivity.java +++ b/app/src/main/java/org/indywidualni/centrumfm/activity/MainActivity.java @@ -282,9 +282,10 @@ protected void onStart() { @Override public void onResume() { - super.onResume(); // Always call the superclass method first + super.onResume(); // schedule RDS updates + rdsLatest = null; rdsHandler.post(rdsRunnable); } diff --git a/app/src/main/java/org/indywidualni/centrumfm/fragment/FavouriteSongsFragment.java b/app/src/main/java/org/indywidualni/centrumfm/fragment/FavouriteSongsFragment.java index 3c2344e..f043b26 100644 --- a/app/src/main/java/org/indywidualni/centrumfm/fragment/FavouriteSongsFragment.java +++ b/app/src/main/java/org/indywidualni/centrumfm/fragment/FavouriteSongsFragment.java @@ -6,9 +6,9 @@ import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v4.view.MenuItemCompat; -import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.view.ActionMode; import android.support.v7.widget.SearchView; -import android.util.Log; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.Menu; @@ -16,14 +16,17 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.LinearLayout; +import android.widget.FrameLayout; import org.indywidualni.centrumfm.R; import org.indywidualni.centrumfm.activity.SongsActivity; import org.indywidualni.centrumfm.rest.adapter.SongsAdapter; import org.indywidualni.centrumfm.rest.model.Song; +import org.indywidualni.centrumfm.util.database.AsyncWrapper; import org.indywidualni.centrumfm.util.database.DataSource; +import org.indywidualni.centrumfm.util.ui.AnimatedLayoutManager; import org.indywidualni.centrumfm.util.ui.RecyclerViewEmptySupport; +import org.indywidualni.centrumfm.util.ui.SlidingTabLayout; import java.util.ArrayList; import java.util.Collections; @@ -41,8 +44,12 @@ public class FavouriteSongsFragment extends Fragment implements SearchView.OnQue @BindView(R.id.empty_view) View emptyView; private Unbinder unbinder; + private SlidingTabLayout slidingTabLayout; + private IFragmentToActivity mCallback; private List songs = new ArrayList<>(); + private ActionModeCallback actionModeCallback = new ActionModeCallback(); + private ActionMode actionMode; private SongsAdapter adapter; @Override @@ -73,6 +80,7 @@ public void onDetach() { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_song_favourite, container, false); + slidingTabLayout = ButterKnife.findById(getActivity(), R.id.tabs); unbinder = ButterKnife.bind(this, view); return view; } @@ -85,10 +93,11 @@ public void onViewCreated(View view, Bundle savedInstanceState) { songs = DataSource.getInstance().getFavouriteSongs(); Collections.sort(songs); adapter = new SongsAdapter(this, songs); + adapter.setHasStableIds(true); mRecyclerView.setHasFixedSize(true); mRecyclerView.setEmptyView(emptyView); - mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + mRecyclerView.setLayoutManager(new AnimatedLayoutManager(getActivity())); mRecyclerView.setAdapter(adapter); } @@ -193,20 +202,29 @@ public boolean onQueryTextSubmit(String query) { } @Override - public void onContentClick(LinearLayout caller, int position) { - SongsActivity.currentPosition = position; - getActivity().openContextMenu(caller); + public void onContentClick(FrameLayout caller, int position) { + if (actionMode != null) + toggleSelection(position); + else { + SongsActivity.currentPosition = position; + getActivity().openContextMenu(caller); + } } @Override public boolean onContentLongClick(int position) { - Log.e("Long Click", "Item: " + position); + if (actionMode == null) { + actionMode = ((AppCompatActivity) getActivity()) + .startSupportActionMode(actionModeCallback); + } + toggleSelection(position); return true; } @Override public void onDestroyView() { super.onDestroyView(); + slidingTabLayout = null; unbinder.unbind(); } @@ -216,4 +234,69 @@ public interface IFragmentToActivity { void shareSong(Song song); } + /** + * Toggle the selection state of an item. + * + * If the item was the last one in the selection and is unselected, the selection is stopped. + * Note that the selection must already be started (actionMode must not be null). + * + * @param position Position of the item to toggle the selection state + */ + private void toggleSelection(int position) { + adapter.toggleSelection(position); + int count = adapter.getSelectedItemCount(); + + if (count == 0) { + actionMode.finish(); + } else { + actionMode.setTitle(getResources().getQuantityString(R.plurals.songs_n_selected, + count, count)); + actionMode.invalidate(); + } + } + + private class ActionModeCallback implements ActionMode.Callback { + @SuppressWarnings("unused") + private final String TAG = ActionModeCallback.class.getSimpleName(); + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + mode.getMenuInflater().inflate (R.menu.menu_actionmode_selected, menu); + slidingTabLayout.setVisibility(View.GONE); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + switch (item.getItemId()) { + case R.id.delete_selected: + List selected = adapter.getSelectedItems(); + List toRemove = new ArrayList<>(); + for (int position : selected) + toRemove.add(adapter.getDataset().get(position)); + songs.removeAll(toRemove); + adapter.getDataset().removeAll(toRemove); + adapter.notifyDataSetChanged(); + AsyncWrapper.removeFavouriteSongs(toRemove); + mode.finish(); + return true; + + default: + return false; + } + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + adapter.clearSelection(); + actionMode = null; + slidingTabLayout.setVisibility(View.VISIBLE); + } + } + } diff --git a/app/src/main/java/org/indywidualni/centrumfm/fragment/SongArchiveFragment.java b/app/src/main/java/org/indywidualni/centrumfm/fragment/SongArchiveFragment.java index 3e33a42..896b632 100644 --- a/app/src/main/java/org/indywidualni/centrumfm/fragment/SongArchiveFragment.java +++ b/app/src/main/java/org/indywidualni/centrumfm/fragment/SongArchiveFragment.java @@ -23,7 +23,7 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.LinearLayout; +import android.widget.FrameLayout; import org.indywidualni.centrumfm.R; import org.indywidualni.centrumfm.activity.SongsActivity; @@ -358,7 +358,7 @@ public void fabClicked() { } @Override - public void onContentClick(LinearLayout caller, int position) { + public void onContentClick(FrameLayout caller, int position) { SongsActivity.currentPosition = position; getActivity().openContextMenu(caller); } diff --git a/app/src/main/java/org/indywidualni/centrumfm/rest/adapter/SelectableAdapter.java b/app/src/main/java/org/indywidualni/centrumfm/rest/adapter/SelectableAdapter.java new file mode 100644 index 0000000..aa7cbd1 --- /dev/null +++ b/app/src/main/java/org/indywidualni/centrumfm/rest/adapter/SelectableAdapter.java @@ -0,0 +1,74 @@ +package org.indywidualni.centrumfm.rest.adapter; + +import android.support.v7.widget.RecyclerView; +import android.util.SparseBooleanArray; + +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("unused") +public abstract class SelectableAdapter + extends RecyclerView.Adapter { + + private static final String TAG = SelectableAdapter.class.getSimpleName(); + + private SparseBooleanArray selectedItems; + + public SelectableAdapter() { + selectedItems = new SparseBooleanArray(); + } + + /** + * Indicates if the item at position position is selected + * @param position Position of the item to check + * @return true if the item is selected, false otherwise + */ + public boolean isSelected(int position) { + return getSelectedItems().contains(position); + } + + /** + * Toggle the selection status of the item at a given position + * @param position Position of the item to toggle the selection status for + */ + public void toggleSelection(int position) { + if (selectedItems.get(position, false)) { + selectedItems.delete(position); + } else { + selectedItems.put(position, true); + } + notifyItemChanged(position); + } + + /** + * Clear the selection status for all items + */ + public void clearSelection() { + List selection = getSelectedItems(); + selectedItems.clear(); + for (Integer i : selection) { + notifyItemChanged(i); + } + } + + /** + * Count the selected items + * @return Selected items count + */ + public int getSelectedItemCount() { + return selectedItems.size(); + } + + /** + * Indicates the list of selected items + * @return List of selected items ids + */ + public List getSelectedItems() { + List items = new ArrayList<>(selectedItems.size()); + for (int i = 0; i < selectedItems.size(); ++i) { + items.add(selectedItems.keyAt(i)); + } + return items; + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/indywidualni/centrumfm/rest/adapter/SongsAdapter.java b/app/src/main/java/org/indywidualni/centrumfm/rest/adapter/SongsAdapter.java index 4b88fee..5d44286 100644 --- a/app/src/main/java/org/indywidualni/centrumfm/rest/adapter/SongsAdapter.java +++ b/app/src/main/java/org/indywidualni/centrumfm/rest/adapter/SongsAdapter.java @@ -6,7 +6,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.LinearLayout; +import android.widget.FrameLayout; import android.widget.TextView; import org.indywidualni.centrumfm.MyApplication; @@ -16,7 +16,7 @@ import java.util.ArrayList; import java.util.List; -public class SongsAdapter extends RecyclerView.Adapter { +public class SongsAdapter extends SelectableAdapter { private List mDataset; private ViewHolder.IViewHolderClicks viewHolderClicks; @@ -53,6 +53,9 @@ else if (!TextUtils.isEmpty(mDataset.get(position).getSum())) .getString(R.string.played_n_times, mDataset.get(position).getSum())); else viewHolder.getPlayed().setVisibility(View.GONE); + + viewHolder.getSelectedOverlay().setVisibility(isSelected(position) + ? View.VISIBLE : View.INVISIBLE); } // return the size of a dataset (invoked by the layout manager) @@ -61,6 +64,12 @@ public int getItemCount() { return mDataset.size(); } + // to use setHasStableIds(true) + @Override + public long getItemId(int position) { + return mDataset.get(position).hashCode(); + } + // get current dataset public final List getDataset() { return mDataset; @@ -128,34 +137,37 @@ private void applyAndAnimateMovedItems(List newModels) { public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { - private final LinearLayout linearLayout; + private final FrameLayout frameLayout; private final TextView id; private final TextView title; private final TextView artist; private final TextView played; private final TextView duration; + private final View selectedOverlay; + private IViewHolderClicks mListener; public ViewHolder(View v, IViewHolderClicks listener) { super(v); mListener = listener; - linearLayout = (LinearLayout) v.findViewById(R.id.element); + frameLayout = (FrameLayout) v.findViewById(R.id.element); id = (TextView) v.findViewById(R.id.id); title = (TextView) v.findViewById(R.id.title); artist = (TextView) v.findViewById(R.id.artist); played = (TextView) v.findViewById(R.id.played); duration = (TextView) v.findViewById(R.id.duration); + selectedOverlay = v.findViewById(R.id.selectedOverlay); - linearLayout.setOnClickListener(this); - linearLayout.setOnLongClickListener(this); + frameLayout.setOnClickListener(this); + frameLayout.setOnLongClickListener(this); } @Override public void onClick(View v) { if (mListener != null) { - if (v instanceof LinearLayout) - mListener.onContentClick((LinearLayout) v, getAdapterPosition()); + if (v instanceof FrameLayout) + mListener.onContentClick((FrameLayout) v, getAdapterPosition()); } } @@ -184,8 +196,12 @@ public TextView getDuration() { return duration; } + public View getSelectedOverlay() { + return selectedOverlay; + } + public interface IViewHolderClicks { - void onContentClick(LinearLayout caller, int position); + void onContentClick(FrameLayout caller, int position); boolean onContentLongClick(int position); } diff --git a/app/src/main/java/org/indywidualni/centrumfm/util/database/AsyncWrapper.java b/app/src/main/java/org/indywidualni/centrumfm/util/database/AsyncWrapper.java index 786c999..01f293f 100644 --- a/app/src/main/java/org/indywidualni/centrumfm/util/database/AsyncWrapper.java +++ b/app/src/main/java/org/indywidualni/centrumfm/util/database/AsyncWrapper.java @@ -64,4 +64,15 @@ protected Void doInBackground(Void... arg0) { }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } + public static void removeFavouriteSongs(final List songs) { + new AsyncTask() { + @Override + protected Void doInBackground(Void... arg0) { + for (Song song : songs) + DataSource.getInstance().removeFavouriteSong(song.getDbId()); + return null; + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + } diff --git a/app/src/main/res/drawable-hdpi/ic_delete_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_delete_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4a9f769475ae98c44086a5498057c799cdc1eb2e GIT binary patch literal 161 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8hNp{Th{y5d1PNA=hW|4Ewf~tb z9PIip^F(s-FKmRW+J}dc&f#K)gg?ihMxz_^iWbkzL Kb6Mw<&;$SheLUU( literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_delete_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_delete_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..e2f5f35558db0965392b15b5d8950261a8c92aab GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1D^C~45R22v2@-!E>_7CM{eR+v zgY~R6ULP4=Whfl~yJVLAs>BdxJ>K;jR@FB~H~;@Xea5N(zxh7d@H058PdfN2J>(P6 O6b4UMKbLh*2~7YiM=IO^ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..388b5b060af924493b057a63216fe7db75d4435a GIT binary patch literal 151 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DI8PVHkch)?FC64-Fko=L=%?Iw zv$u;+&1tXu2b~*Nwk`N$?;xkbvPe<$)w&zy|9BUkVz|Zlf5mTGpk6SDXXMMa|9v2f u@g#fN+oPXnt4Z(ZaQ*pHX6dTRMUVWXo;a(XeY#{ZNU5i*pUXO@geCyi%r-Ls literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3fcdfdb55ebcba8d2fff8be03ea3518c137e3464 GIT binary patch literal 194 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawIz3$+Lo)8Yy>yn>!GXc$q3uOx zabB)cz0QC}ts}W53MuVBD%UsZPv&gw=#W`?Y0ciWQ2$4FyseJx>{`Oj#KIv^yWsd< zb^!&41_nkD7m4wnX|34#N6cTv0=_gxaj*E+Smb+hj$`D9r9sI`%Q@~%Ssl3i>7S{h gq0!eBm41aw^UfFNx4vv@4|EiRr>mdKI;Vst0RDqR&j0`b literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_delete_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_delete_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..8d322aa9baba09c58b0058760fb7d53a392b63a1 GIT binary patch literal 243 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcgc6quuhE&{od*w9mAp-%|!1oIz zUAkW?IxdlN5p^sPGLtM@^D{+9cy9DT>q|G5dQ4KuY+ck{{`y~aU3Uxr^X+awlIm)m zLim6tF)Yw+SYp*L31Z?03ltA1m;Rr_zhgPmvYG>yq8FYs_ibEqHvUn-r*1=DgDFx+ z?bd02*59bTR4g&(=Y(kztIpQ%Z4JJY^zZNE9?f4`+x0ysZP__5=_$iXi`hvQAj>>m L{an^LB{Ts5t^ZVx literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/list_item_song.xml b/app/src/main/res/layout/list_item_song.xml index 7643e70..9e5fd1d 100644 --- a/app/src/main/res/layout/list_item_song.xml +++ b/app/src/main/res/layout/list_item_song.xml @@ -1,77 +1,90 @@ - + android:focusable="true"> - - - + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingBottom="5dp" + android:paddingTop="5dp"> + android:padding="17dp" + android:textColor="@color/colorTextSecondary" + android:textSize="19sp" /> - + android:layout_weight="1"> + + + + + + + + + android:textSize="17sp" /> - + - + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/player_slider.xml b/app/src/main/res/layout/player_slider.xml index 7826399..05ebf5d 100644 --- a/app/src/main/res/layout/player_slider.xml +++ b/app/src/main/res/layout/player_slider.xml @@ -11,7 +11,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" - android:background="?android:attr/selectableItemBackground" + android:foreground="?android:attr/selectableItemBackground" android:clickable="true" android:paddingBottom="10dp" android:paddingEnd="15dp" @@ -68,7 +68,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" - android:background="?android:attr/selectableItemBackground" + android:foreground="?android:attr/selectableItemBackground" android:clickable="true" android:paddingBottom="10dp" android:paddingEnd="15dp" diff --git a/app/src/main/res/menu/context_menu_fav_songs.xml b/app/src/main/res/menu/context_menu_fav_songs.xml index 025510a..7ae88ea 100644 --- a/app/src/main/res/menu/context_menu_fav_songs.xml +++ b/app/src/main/res/menu/context_menu_fav_songs.xml @@ -1,5 +1,6 @@ + @@ -9,4 +10,5 @@ + \ No newline at end of file diff --git a/app/src/main/res/menu/context_menu_songs.xml b/app/src/main/res/menu/context_menu_songs.xml index 506098f..3b0dbd5 100644 --- a/app/src/main/res/menu/context_menu_songs.xml +++ b/app/src/main/res/menu/context_menu_songs.xml @@ -1,5 +1,6 @@ + @@ -9,4 +10,5 @@ + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_actionmode_selected.xml b/app/src/main/res/menu/menu_actionmode_selected.xml new file mode 100644 index 0000000..46a6dd6 --- /dev/null +++ b/app/src/main/res/menu/menu_actionmode_selected.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_drawer.xml b/app/src/main/res/menu/menu_drawer.xml index e088dd8..ae1b79d 100644 --- a/app/src/main/res/menu/menu_drawer.xml +++ b/app/src/main/res/menu/menu_drawer.xml @@ -1,6 +1,8 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml index 4e59dc9..68ee8df 100644 --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_main.xml @@ -1,3 +1,4 @@ + false true + true + @color/colorPrimaryDark true @android:color/transparent diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index afc8607..9b167c5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -86,6 +86,7 @@ Brak utworu, wstrzymaj konie Widok dnia bieżącego Widok dla %1$s + Usuń zaznaczone Dziennik zmian @@ -100,6 +101,12 @@ Audycja trwa %d godzin + + Jedna piosenka + %d piosenki + %d piosenek + + Niedziela Poniedziałek diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 9cd6987..54eb6a7 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -11,7 +11,8 @@