Last active
January 21, 2021 15:31
-
-
Save vkay94/78f6fb2126cf2b39edda483f316aa3c1 to your computer and use it in GitHub Desktop.
NewPipe Local lists with Groupie
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This fragment is design to be used with persistent data such as | |
* {@link org.schabi.newpipe.database.LocalItem}, and does not cache the data contained | |
* in the list adapter to avoid extra writes when the it exits or re-enters its lifecycle. | |
* <p> | |
* This fragment destroys its adapter and views when {@link Fragment#onDestroyView()} is | |
* called and is memory efficient when in backstack. | |
* </p> | |
* | |
* @param <I> List of {@link org.schabi.newpipe.database.LocalItem}s | |
* @param <N> {@link Void} | |
*/ | |
public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I> | |
implements ListViewContract<I, N>, SharedPreferences.OnSharedPreferenceChangeListener { | |
/*////////////////////////////////////////////////////////////////////////// | |
// Views | |
//////////////////////////////////////////////////////////////////////////*/ | |
private static final int LIST_MODE_UPDATE_FLAG = 0x32; | |
protected LocalListGroupAdapter<I> itemListAdapter; | |
protected RecyclerView itemsList; | |
private int updateFlags = 0; | |
protected DateTimeFormatter dateTimeFormatter; | |
/*////////////////////////////////////////////////////////////////////////// | |
// Lifecycle - Creation | |
//////////////////////////////////////////////////////////////////////////*/ | |
@Override | |
public void onCreate(final Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
setHasOptionsMenu(true); | |
PreferenceManager.getDefaultSharedPreferences(activity) | |
.registerOnSharedPreferenceChangeListener(this); | |
} | |
@Override | |
public void onDestroy() { | |
super.onDestroy(); | |
PreferenceManager.getDefaultSharedPreferences(activity) | |
.unregisterOnSharedPreferenceChangeListener(this); | |
} | |
@Override | |
public void onResume() { | |
super.onResume(); | |
if (updateFlags != 0) { | |
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) { | |
itemListAdapter.useGrid(requireContext(), isGridLayout(), getSpanSize(), itemsList); | |
itemListAdapter.updateItemVersion( | |
isGridLayout() ? ItemVersion.GRID : ItemVersion.NORMAL | |
); | |
} | |
updateFlags = 0; | |
} | |
} | |
/*////////////////////////////////////////////////////////////////////////// | |
// Lifecycle - View | |
//////////////////////////////////////////////////////////////////////////*/ | |
// Loading | |
protected Item<GroupieViewHolder> getLoadingListFooter() { | |
return new LoadingItem(); | |
} | |
protected int getSpanSize() { | |
final Resources resources = activity.getResources(); | |
int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width); | |
width += (24 * resources.getDisplayMetrics().density); | |
return (int) Math.floor(resources.getDisplayMetrics().widthPixels / (double) width); | |
} | |
public abstract List<Item<GroupieViewHolder>> fromLocalItemsToGroupie(I items); | |
@Override | |
protected void initViews(final View rootView, final Bundle savedInstanceState) { | |
super.initViews(rootView, savedInstanceState); | |
dateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) | |
.withLocale(Localization.getPreferredLocale(requireContext())); | |
itemListAdapter = new LocalListGroupAdapter<>(this::fromLocalItemsToGroupie); | |
itemsList = rootView.findViewById(R.id.items_list); | |
itemListAdapter.useGrid(requireContext(), isGridLayout(), getSpanSize(), itemsList); | |
itemsList.setAdapter(itemListAdapter); | |
// Set loading indicator as default footer | |
itemListAdapter.setFooter(getLoadingListFooter()); | |
} | |
@Override | |
protected void initListeners() { | |
super.initListeners(); | |
} | |
/*////////////////////////////////////////////////////////////////////////// | |
// Lifecycle - Menu | |
//////////////////////////////////////////////////////////////////////////*/ | |
@Override | |
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { | |
super.onCreateOptionsMenu(menu, inflater); | |
if (DEBUG) { | |
Log.d(TAG, "onCreateOptionsMenu() called with: " | |
+ "menu = [" + menu + "], inflater = [" + inflater + "]"); | |
} | |
final ActionBar supportActionBar = activity.getSupportActionBar(); | |
if (supportActionBar == null) { | |
return; | |
} | |
supportActionBar.setDisplayShowTitleEnabled(true); | |
} | |
/*////////////////////////////////////////////////////////////////////////// | |
// Lifecycle - Destruction | |
//////////////////////////////////////////////////////////////////////////*/ | |
@Override | |
public void onDestroyView() { | |
super.onDestroyView(); | |
itemsList = null; | |
itemListAdapter = null; | |
} | |
/*////////////////////////////////////////////////////////////////////////// | |
// Contract | |
//////////////////////////////////////////////////////////////////////////*/ | |
@Override | |
public void startLoading(final boolean forceLoad) { | |
super.startLoading(forceLoad); | |
resetFragment(); | |
} | |
@Override | |
public void showLoading() { | |
super.showLoading(); | |
// if (itemsList != null) { | |
// animateView(itemsList, false, 200); | |
// } | |
// Not required since the header is within the itemListAdapter | |
// if (headerRootView != null) { | |
// animateView(headerRootView, false, 200); | |
// } | |
} | |
@Override | |
public void hideLoading() { | |
super.hideLoading(); | |
showListFooter(false); | |
// if (itemsList != null) { | |
// animateView(itemsList, true, 200); | |
// } | |
// Not required since the footer is within the itemListAdapter | |
// if (headerRootView != null) { | |
// animateView(headerRootView, true, 200); | |
// } | |
} | |
@Override | |
public void showError(final String message, final boolean showRetryButton) { | |
super.showError(message, showRetryButton); | |
showListFooter(false); | |
// if (itemsList != null) { | |
// animateView(itemsList, false, 200); | |
// } | |
// Not required since the header is within the itemListAdapter | |
// if (headerRootView != null) { | |
// animateView(headerRootView, false, 200); | |
// } | |
} | |
@Override | |
public void showEmptyState() { | |
super.showEmptyState(); | |
showListFooter(false); | |
} | |
@Override | |
public void showListFooter(final boolean show) { | |
if (itemsList == null) { | |
return; | |
} | |
itemsList.post(() -> { | |
if (itemListAdapter != null) { | |
itemListAdapter.showFooter(show); | |
} | |
}); | |
} | |
@Override | |
public void handleNextItems(final N result) { | |
isLoading.set(false); | |
} | |
/*////////////////////////////////////////////////////////////////////////// | |
// Error handling | |
//////////////////////////////////////////////////////////////////////////*/ | |
protected void resetFragment() { | |
// TODO: Do not clear items always, better: update items if needed in those places | |
if (itemListAdapter != null) { | |
itemListAdapter.clearItems(); | |
} | |
} | |
@Override | |
protected boolean onError(final Throwable exception) { | |
// resetFragment(); | |
itemListAdapter.clearItems(); | |
return super.onError(exception); | |
} | |
@Override | |
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, | |
final String key) { | |
if (key.equals(getString(R.string.list_view_mode_key))) { | |
updateFlags |= LIST_MODE_UPDATE_FLAG; | |
} | |
} | |
protected boolean isGridLayout() { | |
final String listMode = PreferenceManager.getDefaultSharedPreferences(activity) | |
.getString(getString(R.string.list_view_mode_key), | |
getString(R.string.list_view_mode_value)); | |
if ("auto".equals(listMode)) { | |
final Configuration configuration = getResources().getConfiguration(); | |
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE | |
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE); | |
} else { | |
return "grid".equals(listMode); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
abstract class DynamicGridItem : Item<GroupieViewHolder>() { | |
var itemVersion: ItemVersion = ItemVersion.NORMAL | |
@get:LayoutRes | |
abstract val normalLayout: Int | |
@get:LayoutRes | |
abstract val miniLayout: Int | |
@get:LayoutRes | |
abstract val gridLayout: Int | |
override fun getLayout(): Int = when (itemVersion) { | |
ItemVersion.NORMAL -> normalLayout | |
ItemVersion.MINI -> miniLayout | |
ItemVersion.GRID -> gridLayout | |
} | |
override fun getSpanSize(spanCount: Int, position: Int): Int { | |
return if (itemVersion == ItemVersion.GRID) 1 else spanCount | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Custom GroupAdapter which provides a header, item and footer section | |
*/ | |
class LocalListGroupAdapter<T>( | |
private val convertItems: (result: T) -> List<Item<GroupieViewHolder>> | |
) : GroupAdapter<GroupieViewHolder>() { | |
private val DEBUG = MainActivity.DEBUG | |
private val TAG = javaClass.simpleName | |
private val itemsSection = Section() | |
private val headerSection = Section() | |
private val footerSection = Section() | |
// Required for showFooter(show) | |
private var header: Group? = null | |
private var footer: Group? = null | |
private var showFooter = false | |
private var itemVersion = ItemVersion.NORMAL | |
fun itemsSection() = itemsSection | |
fun isGrid() = itemVersion == ItemVersion.GRID | |
init { | |
add(headerSection) | |
add(itemsSection) | |
add(footerSection) | |
} | |
fun setHeader(header: Item<GroupieViewHolder>?) { | |
if (header == null) { | |
if (DEBUG) Log.e(TAG, "setHeader(header): Header is null") | |
return | |
} | |
clearHeader() | |
this.header = header | |
headerSection.add(header) | |
} | |
fun clearHeader() { | |
headerSection.clear() | |
header = null | |
} | |
fun setFooter(footer: Item<GroupieViewHolder>?) { | |
if (footer == null) { | |
if (DEBUG) Log.e(TAG, "setFooter(footer): Footer is null") | |
return | |
} | |
clearFooter() | |
this.footer = footer | |
// footerSection.add(footer) | |
} | |
fun clearFooter() { | |
footerSection.clear() | |
footer = null | |
showFooter = false | |
} | |
fun showFooter(show: Boolean) { | |
if (DEBUG) Log.d(TAG, "showFooter(show) called with show = [$show]") | |
if (show && footer != null && footerSection.groups.indexOf(footer) == -1) { | |
footer?.let { | |
footerSection.add(it) | |
showFooter = true | |
} | |
} else { | |
clearFooter() | |
} | |
} | |
fun isItemListEmpty(): Boolean = itemsSection.itemCount == 0 | |
fun updateItems(items: T) { | |
updateItems(items, false) | |
} | |
fun updateItems(items: T, refresh: Boolean) { | |
when { | |
isItemListEmpty() -> { | |
itemsSection.addAll(convertItems.invoke(items)) | |
} | |
refresh -> { | |
// itemsSection.clear() | |
// itemsSection.addAll(convertItems.invoke(items)) | |
itemsSection.refresh(convertItems.invoke(items)) | |
} | |
else -> { | |
itemsSection.update(convertItems.invoke(items)) | |
} | |
} | |
} | |
fun addMoreItems(items: T) { | |
itemsSection.addAll(convertItems.invoke(items)) | |
} | |
fun clearItems() { | |
itemsSection.clear() | |
} | |
fun swapItems(viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { | |
val item = this.getItem(viewHolder.adapterPosition) | |
val targetItem = this.getItem(target.adapterPosition) | |
val currentItems = itemsSection.groups | |
var targetIndex = currentItems.indexOf(targetItem) | |
currentItems.remove(item) | |
if (targetIndex == -1) { | |
targetIndex = if (target.adapterPosition < viewHolder.adapterPosition) | |
0 | |
else | |
currentItems.size - 1 | |
} | |
currentItems.add(targetIndex, item) | |
itemsSection.update(currentItems) | |
return true | |
} | |
fun itemIndexOf(item: Item<GroupieViewHolder>): Int { | |
// TODO: With adapterPosition? | |
return itemsSection.getPosition(item) | |
} | |
fun useGrid(context: Context, shouldUseGrid: Boolean, spanSize: Int, recyclerView: RecyclerView) { | |
spanCount = if (shouldUseGrid) spanSize else 1 | |
val newLayoutManager = GridLayoutManager(context, spanCount) | |
newLayoutManager.spanSizeLookup = spanSizeLookup | |
recyclerView.layoutManager = newLayoutManager | |
} | |
fun updateItemVersion(itemVersion: ItemVersion) { | |
this.itemVersion = itemVersion | |
val updatedList: List<Group> = itemsSection.groups.map { | |
if (it is DynamicGridItem) it.itemVersion = itemVersion | |
it | |
} | |
itemsSection.clear() | |
itemsSection.addAll(updatedList) | |
// itemsSection.update(updatedList) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment