From 69f1c9af4a914314bfa9c6281adbaa8f12160cfe Mon Sep 17 00:00:00 2001 From: Denis Karmyshakov Date: Fri, 16 Mar 2018 17:54:36 +0300 Subject: [PATCH] Added DelegationListAdapter, small refactor of delegates --- .../components/adapters/AdapterDelegate.java | 41 +++++++ .../components/adapters/DelegatesManager.kt | 60 ++++++++++ .../adapters/DelegationListAdapter.kt | 82 ++++++++++++++ .../adapters/ItemAdapterDelegate.java | 86 +++++++------- .../adapters/OffsetAdapterUpdateCallback.kt | 24 ++++ .../adapters/PositionAdapterDelegate.java | 52 +++++---- .../roboswag/components/utils/Typefaces.java | 105 ------------------ 7 files changed, 278 insertions(+), 172 deletions(-) create mode 100644 src/main/java/ru/touchin/roboswag/components/adapters/DelegatesManager.kt create mode 100644 src/main/java/ru/touchin/roboswag/components/adapters/DelegationListAdapter.kt create mode 100644 src/main/java/ru/touchin/roboswag/components/adapters/OffsetAdapterUpdateCallback.kt delete mode 100644 src/main/java/ru/touchin/roboswag/components/utils/Typefaces.java diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java b/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java index ee6155c..25be0c9 100644 --- a/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java +++ b/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java @@ -24,6 +24,8 @@ import android.support.v4.view.ViewCompat; import android.support.v7.widget.RecyclerView; import android.view.ViewGroup; +import java.util.List; + /** * Objects of such class controls creation and binding of specific type of RecyclerView's ViewHolders. * Default {@link #getItemViewType} is generating on construction of object. @@ -43,6 +45,28 @@ public abstract class AdapterDelegate items, final int adapterPosition, final int collectionPosition); + + /** + * Returns unique ID of item to support stable ID's logic of RecyclerView's adapter. + * + * @param items Items in adapter; + * @param adapterPosition Position of item in adapter; + * @param collectionPosition Position of item in collection; + * @return Unique item ID. + */ + public long getItemId(@NonNull final List items, final int adapterPosition, final int collectionPosition) { + return 0; + } + /** * Creates ViewHolder to bind item to it later. * @@ -52,4 +76,21 @@ public abstract class AdapterDelegate items, + final int adapterPosition, + final int collectionPosition, + @NonNull final List payloads + ); + } diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/DelegatesManager.kt b/src/main/java/ru/touchin/roboswag/components/adapters/DelegatesManager.kt new file mode 100644 index 0000000..bb70eb3 --- /dev/null +++ b/src/main/java/ru/touchin/roboswag/components/adapters/DelegatesManager.kt @@ -0,0 +1,60 @@ +package ru.touchin.roboswag.components.adapters + +import android.support.v7.widget.RecyclerView +import android.util.SparseArray +import android.view.ViewGroup + +/** + * Manager for delegation callbacks from [RecyclerView.Adapter] to delegates. + */ +class DelegatesManager { + + private val delegates = SparseArray>() + + fun getItemViewType(items: List<*>, adapterPosition: Int, collectionPosition: Int): Int { + for (index in 0 until delegates.size()) { + val delegate = delegates.valueAt(index) + if (delegate.isForViewType(items, adapterPosition, collectionPosition)) { + return delegate.itemViewType + } + } + throw IllegalStateException("Delegate not found") + } + + fun getItemId(items: List<*>, adapterPosition: Int, collectionPosition: Int): Long { + val delegate = getDelegate(getItemViewType(items, adapterPosition, collectionPosition)) + return delegate.getItemId(items, adapterPosition, collectionPosition) + } + + fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = getDelegate(viewType).onCreateViewHolder(parent) + + fun onBindViewHolder(holder: RecyclerView.ViewHolder, items: List<*>, adapterPosition: Int, collectionPosition: Int, payloads: List) { + val delegate = getDelegate(getItemViewType(items, adapterPosition, collectionPosition)) + delegate.onBindViewHolder(holder, items, adapterPosition, collectionPosition, payloads) + } + + /** + * Adds [ItemAdapterDelegate] to adapter. + * + * @param delegate Delegate to add. + */ + fun addDelegate(delegate: ItemAdapterDelegate<*, *>) = delegates.put(delegate.itemViewType, delegate) + + /** + * Adds [PositionAdapterDelegate] to adapter. + * + * @param delegate Delegate to add. + */ + fun addDelegate(delegate: PositionAdapterDelegate<*>) = delegates.put(delegate.itemViewType, delegate) + + /** + * Removes [AdapterDelegate] from adapter. + * + * @param delegate Delegate to remove. + */ + fun removeDelegate(delegate: AdapterDelegate<*>) = delegates.remove(delegate.itemViewType) + + private fun getDelegate(viewType: Int): AdapterDelegate<*> = + delegates[viewType] ?: throw IllegalStateException("No AdapterDelegate added for view type: $viewType") + +} diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/DelegationListAdapter.kt b/src/main/java/ru/touchin/roboswag/components/adapters/DelegationListAdapter.kt new file mode 100644 index 0000000..24e56fb --- /dev/null +++ b/src/main/java/ru/touchin/roboswag/components/adapters/DelegationListAdapter.kt @@ -0,0 +1,82 @@ +package ru.touchin.roboswag.components.adapters + +import android.support.v7.recyclerview.extensions.AsyncDifferConfig +import android.support.v7.recyclerview.extensions.AsyncListDiffer +import android.support.v7.util.DiffUtil +import android.support.v7.widget.RecyclerView +import android.view.ViewGroup + +/** + * Base adapter with delegation and diff computing on background thread. + */ +open class DelegationListAdapter(diffCallback: DiffUtil.ItemCallback) : RecyclerView.Adapter() { + + var itemClickListener: ((TItem, RecyclerView.ViewHolder) -> Unit)? = null + + private val delegatesManager = DelegatesManager() + private var differ = AsyncListDiffer(OffsetAdapterUpdateCallback(this, ::getHeadersCount), AsyncDifferConfig.Builder(diffCallback).build()) + + open fun getHeadersCount(): Int = 0 + + open fun getFootersCount(): Int = 0 + + override fun getItemCount(): Int = getHeadersCount() + differ.currentList.size + getFootersCount() + + override fun getItemViewType(position: Int): Int = delegatesManager.getItemViewType(getList(), position, getCollectionPosition(position)) + + override fun getItemId(position: Int): Long = delegatesManager.getItemId(getList(), position, getCollectionPosition(position)) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = delegatesManager.onCreateViewHolder(parent, viewType) + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List) { + val collectionPosition = getCollectionPosition(position) + if (itemClickListener != null && collectionPosition in 0 until getList().size) { + holder.itemView.setOnClickListener { itemClickListener?.invoke(getList()[collectionPosition], holder) } + } else { + holder.itemView.setOnClickListener(null) + } + delegatesManager.onBindViewHolder(holder, getList(), position, collectionPosition, payloads) + } + + final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) = Unit + + /** + * Adds [ItemAdapterDelegate] to adapter. + * + * @param delegate Delegate to add. + */ + fun addDelegate(delegate: ItemAdapterDelegate<*, *>) = delegatesManager.addDelegate(delegate) + + /** + * Adds [PositionAdapterDelegate] to adapter. + * + * @param delegate Delegate to add. + */ + fun addDelegate(delegate: PositionAdapterDelegate<*>) = delegatesManager.addDelegate(delegate) + + /** + * Submits a new list to be diffed, and displayed. + * + * If a list is already being displayed, a diff will be computed on a background thread, which + * will dispatch Adapter.notifyItem events on the main thread. + * + * @param list The new list to be displayed. + */ + fun submitList(list: List) = differ.submitList(list) + + /** + * Get the current List - any diffing to present this list has already been computed and + * dispatched via the ListUpdateCallback. + *

+ * If a null List, or no List has been submitted, an empty list will be returned. + *

+ * The returned list may not be mutated - mutations to content must be done through + * {@link #submitList(List)}. + * + * @return current List. + */ + fun getList(): List = differ.currentList + + fun getCollectionPosition(adapterPosition: Int): Int = adapterPosition - getHeadersCount() + +} diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/ItemAdapterDelegate.java b/src/main/java/ru/touchin/roboswag/components/adapters/ItemAdapterDelegate.java index b7a3a44..44a9902 100644 --- a/src/main/java/ru/touchin/roboswag/components/adapters/ItemAdapterDelegate.java +++ b/src/main/java/ru/touchin/roboswag/components/adapters/ItemAdapterDelegate.java @@ -2,7 +2,6 @@ package ru.touchin.roboswag.components.adapters; import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; -import android.view.ViewGroup; import java.util.List; @@ -14,72 +13,73 @@ import java.util.List; * @param Type of {@link RecyclerView.ViewHolder} of delegate; * @param Type of items to bind to {@link RecyclerView.ViewHolder}s. */ +@SuppressWarnings("unchecked") public abstract class ItemAdapterDelegate extends AdapterDelegate { + @Override + public boolean isForViewType(@NonNull final List items, final int adapterPosition, final int collectionPosition) { + return collectionPosition >= 0 + && collectionPosition < items.size() + && isForViewType(items.get(collectionPosition), adapterPosition, collectionPosition); + } + /** * Returns if object is processable by this delegate. - * This item will be casted to {@link TItem} and passes to {@link #onBindViewHolder(TViewHolder, TItem, int, int)}. + * This item will be casted to {@link TItem} and passes to {@link #onBindViewHolder(TViewHolder, TItem, int, int, List)}. * - * @param item Item to check; - * @param positionInAdapter Position of item in adapter; - * @param itemCollectionPosition Position of item in collection that contains item; + * @param item Item to check; + * @param adapterPosition Position of item in adapter; + * @param collectionPosition Position of item in collection that contains item; * @return True if item is processable by this delegate. */ - public abstract boolean isForViewType(@NonNull final Object item, final int positionInAdapter, final int itemCollectionPosition); + public boolean isForViewType(@NonNull final Object item, final int adapterPosition, final int collectionPosition) { + return true; + } + + @Override + public long getItemId(@NonNull final List items, final int adapterPosition, final int collectionPosition) { + return getItemId((TItem) items.get(collectionPosition), adapterPosition, collectionPosition); + } /** * Returns unique ID of item to support stable ID's logic of RecyclerView's adapter. * - * @param item Item to check; - * @param positionInAdapter Position of item in adapter; - * @param positionInCollection Position of item in collection that contains item; + * @param item Item in adapter; + * @param adapterPosition Position of item in adapter; + * @param collectionPosition Position of item in collection that contains item; * @return Unique item ID. */ - public long getItemId(@NonNull final TItem item, final int positionInAdapter, final int positionInCollection) { + public long getItemId(@NonNull final TItem item, final int adapterPosition, final int collectionPosition) { return 0; } - /** - * Creates ViewHolder to bind item to it later. - * - * @param parent Container of ViewHolder's view. - * @return New ViewHolder. - */ - @NonNull - public abstract TViewHolder onCreateViewHolder(@NonNull final ViewGroup parent); - - /** - * Binds item to created by this object ViewHolder. - * - * @param holder ViewHolder to bind item to; - * @param item Item to check; - * @param positionInAdapter Position of item in adapter; - * @param positionInCollection Position of item in collection that contains item; - */ - public abstract void onBindViewHolder( - @NonNull final TViewHolder holder, - @NonNull final TItem item, - final int positionInAdapter, - final int positionInCollection - ); + @Override + public void onBindViewHolder( + @NonNull final RecyclerView.ViewHolder holder, + @NonNull final List items, + final int adapterPosition, + final int collectionPosition, + @NonNull final List payloads + ) { + //noinspection unchecked + onBindViewHolder((TViewHolder) holder, (TItem) items.get(collectionPosition), adapterPosition, collectionPosition, payloads); + } /** * Binds item with payloads to created by this object ViewHolder. * * @param holder ViewHolder to bind item to; - * @param item Item to check; + * @param item Item in adapter; + * @param adapterPosition Position of item in adapter; + * @param collectionPosition Position of item in collection that contains item; * @param payloads Payloads; - * @param positionInAdapter Position of item in adapter; - * @param positionInCollection Position of item in collection that contains item; */ - public void onBindViewHolder( + public abstract void onBindViewHolder( @NonNull final TViewHolder holder, @NonNull final TItem item, - @NonNull final List payloads, - final int positionInAdapter, - final int positionInCollection - ) { - //do nothing by default - } + final int adapterPosition, + final int collectionPosition, + @NonNull final List payloads + ); } diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/OffsetAdapterUpdateCallback.kt b/src/main/java/ru/touchin/roboswag/components/adapters/OffsetAdapterUpdateCallback.kt new file mode 100644 index 0000000..73bc999 --- /dev/null +++ b/src/main/java/ru/touchin/roboswag/components/adapters/OffsetAdapterUpdateCallback.kt @@ -0,0 +1,24 @@ +package ru.touchin.roboswag.components.adapters + +import android.support.v7.util.ListUpdateCallback +import android.support.v7.widget.RecyclerView + +class OffsetAdapterUpdateCallback(private val adapter: RecyclerView.Adapter<*>, private val offsetProvider: () -> Int) : ListUpdateCallback { + + override fun onInserted(position: Int, count: Int) { + adapter.notifyItemInserted(position + offsetProvider()) + } + + override fun onRemoved(position: Int, count: Int) { + adapter.notifyItemRangeRemoved(position + offsetProvider(), count) + } + + override fun onMoved(fromPosition: Int, toPosition: Int) { + adapter.notifyItemMoved(fromPosition + offsetProvider(), toPosition + offsetProvider()) + } + + override fun onChanged(position: Int, count: Int, payload: Any?) { + adapter.notifyItemRangeChanged(position + offsetProvider(), count, payload) + } + +} diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/PositionAdapterDelegate.java b/src/main/java/ru/touchin/roboswag/components/adapters/PositionAdapterDelegate.java index ac83c51..ada8888 100644 --- a/src/main/java/ru/touchin/roboswag/components/adapters/PositionAdapterDelegate.java +++ b/src/main/java/ru/touchin/roboswag/components/adapters/PositionAdapterDelegate.java @@ -2,7 +2,6 @@ package ru.touchin.roboswag.components.adapters; import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; -import android.view.ViewGroup; import java.util.List; @@ -15,49 +14,54 @@ import java.util.List; */ public abstract class PositionAdapterDelegate extends AdapterDelegate { + @Override + public boolean isForViewType(@NonNull final List items, final int adapterPosition, final int collectionPosition) { + return isForViewType(adapterPosition); + } + /** * Returns if object is processable by this delegate. * - * @param positionInAdapter Position of item in adapter; + * @param adapterPosition Position of item in adapter; * @return True if item is processable by this delegate. */ - public abstract boolean isForViewType(final int positionInAdapter); + public abstract boolean isForViewType(final int adapterPosition); + + @Override + public long getItemId(@NonNull final List objects, final int adapterPosition, final int itemsOffset) { + return getItemId(adapterPosition); + } /** * Returns unique ID of item to support stable ID's logic of RecyclerView's adapter. * - * @param positionInAdapter Position of item in adapter; + * @param adapterPosition Position of item in adapter; * @return Unique item ID. */ - public long getItemId(final int positionInAdapter) { + public long getItemId(final int adapterPosition) { return 0; } - /** - * Creates ViewHolder to bind position to it later. - * - * @param parent Container of ViewHolder's view. - * @return New ViewHolder. - */ - @NonNull - public abstract TViewHolder onCreateViewHolder(@NonNull final ViewGroup parent); - - /** - * Binds position to ViewHolder. - * - * @param holder ViewHolder to bind position to; - * @param positionInAdapter Position of item in adapter. - */ - public abstract void onBindViewHolder(@NonNull final TViewHolder holder, final int positionInAdapter); + @Override + public void onBindViewHolder( + @NonNull final RecyclerView.ViewHolder holder, + @NonNull final List items, + final int adapterPosition, + final int collectionPosition, + @NonNull final List payloads + ) { + //noinspection unchecked + onBindViewHolder((TViewHolder) holder, adapterPosition, payloads); + } /** * Binds position with payloads to ViewHolder. * * @param holder ViewHolder to bind position to; - * @param payloads Payloads; - * @param positionInAdapter Position of item in adapter. + * @param adapterPosition Position of item in adapter; + * @param payloads Payloads. */ - public void onBindViewHolder(@NonNull final TViewHolder holder, @NonNull final List payloads, final int positionInAdapter) { + public void onBindViewHolder(@NonNull final TViewHolder holder, final int adapterPosition, @NonNull final List payloads) { //do nothing by default } diff --git a/src/main/java/ru/touchin/roboswag/components/utils/Typefaces.java b/src/main/java/ru/touchin/roboswag/components/utils/Typefaces.java deleted file mode 100644 index d8be010..0000000 --- a/src/main/java/ru/touchin/roboswag/components/utils/Typefaces.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) - * - * This file is part of RoboSwag library. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package ru.touchin.roboswag.components.utils; - -import android.content.Context; -import android.content.res.AssetManager; -import android.content.res.TypedArray; -import android.graphics.Typeface; -import android.support.annotation.NonNull; -import android.support.annotation.StyleableRes; -import android.util.AttributeSet; - -import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import ru.touchin.roboswag.core.log.Lc; -import ru.touchin.roboswag.core.utils.ShouldNotHappenException; - -/** - * Created by Gavriil Sitnikov on 18/07/2014. - * Manager for typefaces stored in 'assets/fonts' folder. - */ -public final class Typefaces { - - private static final Map TYPEFACES_MAP = new HashMap<>(); - - /** - * Returns {@link Typeface} by name from assets 'fonts' folder. - * - * @param context Context of assets where typeface file stored in; - * @param name Full name of typeface (without extension, e.g. 'Roboto-Regular'); - * @return {@link Typeface} from assets. - */ - @NonNull - public static Typeface getByName(@NonNull final Context context, @NonNull final String name) { - synchronized (TYPEFACES_MAP) { - Typeface result = TYPEFACES_MAP.get(name); - if (result == null) { - final AssetManager assetManager = context.getAssets(); - result = Typeface.DEFAULT; - try { - final List fonts = Arrays.asList(assetManager.list("fonts")); - if (fonts.contains(name + ".ttf")) { - result = Typeface.createFromAsset(assetManager, "fonts/" + name + ".ttf"); - } else if (fonts.contains(name + ".otf")) { - result = Typeface.createFromAsset(assetManager, "fonts/" + name + ".otf"); - } else { - Lc.assertion("Can't find .otf or .ttf file in assets folder 'fonts' with name: " + name); - } - } catch (final IOException exception) { - Lc.assertion(new ShouldNotHappenException("Can't get font " + name + '.' - + "Did you forget to create assets folder named 'fonts'?", exception)); - } - TYPEFACES_MAP.put(name, result); - } - return result; - } - } - - /** - * Returns {@link Typeface} by name from assets 'fonts' folder. - * - * @param context Context of assets where typeface file stored in; - * @param attrs Attributes of view to get font from; - * @param styleableId Id of attribute set; - * @param attributeId Id of attribute; - * @return {@link Typeface} from assets. - */ - @NonNull - public static Typeface getFromAttributes(@NonNull final Context context, @NonNull final AttributeSet attrs, - @NonNull @StyleableRes final int[] styleableId, @StyleableRes final int attributeId) { - final TypedArray typedArray = context.obtainStyledAttributes(attrs, styleableId); - final String customTypeface = typedArray.getString(attributeId); - typedArray.recycle(); - if (customTypeface != null) { - return getByName(context, customTypeface); - } - Lc.w("Couldn't find custom typeface. Returns default"); - return Typeface.DEFAULT; - } - - private Typefaces() { - } - -} \ No newline at end of file