Added modules: recyclerview-adapters, kotlin-extensions

This commit is contained in:
Denis Karmyshakov 2018-08-21 16:45:56 +03:00
parent 1d6d6d06be
commit 7be175f750
22 changed files with 560 additions and 26 deletions

View File

@ -18,7 +18,7 @@ repositories {
}
dependencies {
api project(":components-storable")
api project(":storable")
api 'net.danlew:android.joda:2.9.9.4'
implementation "com.android.support:support-annotations:$versions.supportLibrary"

1
kotlin-extensions/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,16 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion versions.compileSdk
defaultConfig {
minSdkVersion versions.minSdk
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.android.support:recyclerview-v7:$versions.supportLibrary"
}

View File

@ -0,0 +1,2 @@
<manifest
package="ru.touchin.roboswag.components.extensions"/>

View File

@ -0,0 +1,24 @@
package ru.touchin.roboswag.components.extensions
import kotlin.properties.Delegates
import kotlin.properties.ObservableProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/**
* Simple observable delegate only for notification of new value.
*/
inline fun <T> Delegates.observable(
initialValue: T,
crossinline onChange: (newValue: T) -> Unit
): ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(newValue)
}
inline fun <T> Delegates.distinctUntilChanged(
initialValue: T,
crossinline onChange: (newValue: T) -> Unit
): ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) =
if (newValue != null && oldValue != newValue) onChange(newValue) else Unit
}

View File

@ -0,0 +1,20 @@
package ru.touchin.roboswag.components.extensions
import android.os.Build
import android.view.View
private const val RIPPLE_EFFECT_DELAY = 150L
/**
* Sets click listener to view. On click it will call something after delay.
*
* @param delay Delay after which click listener will be called;
* @param listener Click listener.
*/
fun View.setOnRippleClickListener(delay: Long = RIPPLE_EFFECT_DELAY, listener: (View) -> Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setOnClickListener { view -> postDelayed({ if (hasWindowFocus()) listener(view) }, delay) }
} else {
setOnClickListener(listener)
}
}

View File

@ -0,0 +1,32 @@
package ru.touchin.roboswag.components.extensions
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.drawable.Drawable
import android.support.annotation.ColorInt
import android.support.annotation.ColorRes
import android.support.annotation.DrawableRes
import android.support.annotation.IdRes
import android.support.annotation.StringRes
import android.support.v4.content.ContextCompat
import android.support.v7.widget.RecyclerView
import android.view.View
fun <T : View> RecyclerView.ViewHolder.findViewById(@IdRes resId: Int): T = itemView.findViewById(resId)
val RecyclerView.ViewHolder.context: Context
get() = itemView.context
fun RecyclerView.ViewHolder.getText(@StringRes resId: Int): CharSequence = context.getText(resId)
fun RecyclerView.ViewHolder.getString(@StringRes resId: Int): String = context.getString(resId)
@SuppressWarnings("SpreadOperator") // it's OK for small arrays
fun RecyclerView.ViewHolder.getString(@StringRes resId: Int, vararg args: Any): String = context.getString(resId, *args)
@ColorInt
fun RecyclerView.ViewHolder.getColor(@ColorRes resId: Int): Int = ContextCompat.getColor(context, resId)
fun RecyclerView.ViewHolder.getColorStateList(@ColorRes resId: Int): ColorStateList? = ContextCompat.getColorStateList(context, resId)
fun RecyclerView.ViewHolder.getDrawable(@DrawableRes resId: Int): Drawable? = ContextCompat.getDrawable(context, resId)

View File

@ -15,7 +15,7 @@ android {
}
dependencies {
api project(":components-navigation")
api project(":navigation")
compileOnly "javax.inject:javax.inject:1"

View File

@ -10,8 +10,8 @@ android {
}
dependencies {
api project(":components-utils")
api project(":components-logging")
api project(":utils")
api project(":logging")
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

View File

@ -5,20 +5,24 @@ if (gradle.ext.has('componentsRoot')) {
rootDir = settingsDir
}
include ':components-logging'
include ':components-utils'
include ':components-navigation'
include ':components-storable'
include ':components-api-logansquare'
include ':components-lifecycle-common'
include ':components-lifecycle-rx'
include ':components-views'
include ':logging'
include ':utils'
include ':navigation'
include ':storable'
include ':api-logansquare'
include ':lifecycle-common'
include ':lifecycle-rx'
include ':views'
include ':recyclerview-adapters'
include ':kotlin-extensions'
project(':components-utils').projectDir = new File(rootDir, 'utils')
project(':components-logging').projectDir = new File(rootDir, 'logging')
project(':components-navigation').projectDir = new File(rootDir, 'navigation')
project(':components-storable').projectDir = new File(rootDir, 'storable')
project(':components-api-logansquare').projectDir = new File(rootDir, 'api-logansquare')
project(':components-lifecycle-common').projectDir = new File(rootDir, 'lifecycle-common')
project(':components-lifecycle-rx').projectDir = new File(rootDir, 'lifecycle-rx')
project(':components-views').projectDir = new File(rootDir, 'views')
project(':utils').projectDir = new File(rootDir, 'utils')
project(':logging').projectDir = new File(rootDir, 'logging')
project(':navigation').projectDir = new File(rootDir, 'navigation')
project(':storable').projectDir = new File(rootDir, 'storable')
project(':api-logansquare').projectDir = new File(rootDir, 'api-logansquare')
project(':lifecycle-common').projectDir = new File(rootDir, 'lifecycle-common')
project(':lifecycle-rx').projectDir = new File(rootDir, 'lifecycle-rx')
project(':views').projectDir = new File(rootDir, 'views')
project(':recyclerview-adapters').projectDir = new File(rootDir, 'recyclerview-adapters')
project(':kotlin-extensions').projectDir = new File(rootDir, 'kotlin-extensions')

View File

@ -15,8 +15,8 @@ android {
}
dependencies {
api project(":components-utils")
api project(":components-logging")
api project(":utils")
api project(":logging")
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

1
recyclerview-adapters/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,18 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion versions.compileSdk
defaultConfig {
minSdkVersion versions.minSdk
}
}
dependencies {
api project(':kotlin-extensions')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.android.support:recyclerview-v7:$versions.supportLibrary"
}

View File

@ -0,0 +1,2 @@
<manifest
package="ru.touchin.roboswag.components.adapters"/>

View File

@ -0,0 +1,96 @@
/*
* Copyright (c) 2017 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.adapters;
import android.support.annotation.NonNull;
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.
*
* @param <TViewHolder> Type of {@link RecyclerView.ViewHolder} of delegate.
*/
public abstract class AdapterDelegate<TViewHolder extends RecyclerView.ViewHolder> {
private final int defaultItemViewType = ViewCompat.generateViewId();
/**
* Unique ID of AdapterDelegate.
*
* @return Unique ID.
*/
public int getItemViewType() {
return defaultItemViewType;
}
/**
* Returns if object is processable by this delegate.
*
* @param items Items to check;
* @param adapterPosition Position of item in adapter;
* @param collectionPosition Position of item in collection;
* @return True if item is processable by this delegate.
*/
public abstract boolean isForViewType(@NonNull final List<Object> 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<Object> items, 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 items Items in adapter;
* @param adapterPosition Position of item in adapter;
* @param collectionPosition Position of item in collection that contains item;
* @param payloads Payloads;
*/
public abstract void onBindViewHolder(
@NonNull final RecyclerView.ViewHolder holder,
@NonNull final List<Object> items,
final int adapterPosition,
final int collectionPosition,
@NonNull final List<Object> payloads
);
}

View File

@ -0,0 +1,52 @@
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<AdapterDelegate<*>>()
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 for adapterPosition: $adapterPosition")
}
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<Any>) {
val delegate = getDelegate(getItemViewType(items, adapterPosition, collectionPosition))
delegate.onBindViewHolder(holder, items, adapterPosition, collectionPosition, payloads)
}
/**
* Adds [PositionAdapterDelegate] to adapter.
*
* @param delegate Delegate to add.
*/
fun addDelegate(delegate: AdapterDelegate<*>) = 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) = delegates[viewType] ?: throw IllegalStateException("No AdapterDelegate added for view type: $viewType")
}

View File

@ -0,0 +1,89 @@
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
import ru.touchin.roboswag.components.extensions.setOnRippleClickListener
/**
* Base adapter with delegation and diff computing on background thread.
*/
open class DelegationListAdapter<TItem>(config: AsyncDifferConfig<TItem>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
constructor(diffCallback: DiffUtil.ItemCallback<TItem>) : this(AsyncDifferConfig.Builder<TItem>(diffCallback).build())
var itemClickListener: ((TItem, RecyclerView.ViewHolder) -> Unit)? = null
private val delegatesManager = DelegatesManager()
private var differ = AsyncListDiffer(OffsetAdapterUpdateCallback(this, ::getHeadersCount), config)
open fun getHeadersCount() = 0
open fun getFootersCount() = 0
override fun getItemCount() = getHeadersCount() + getList().size + getFootersCount()
override fun getItemViewType(position: Int) = delegatesManager.getItemViewType(getList(), position, getCollectionPosition(position))
override fun getItemId(position: Int) = delegatesManager.getItemId(getList(), position, getCollectionPosition(position))
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = delegatesManager.onCreateViewHolder(parent, viewType)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<Any>) {
val collectionPosition = getCollectionPosition(position)
if (collectionPosition in 0 until getList().size) {
if (itemClickListener != null) {
holder.itemView.setOnRippleClickListener {
itemClickListener?.invoke(getList()[getCollectionPosition(holder.adapterPosition)], holder)
}
} else {
holder.itemView.setOnClickListener(null)
}
}
delegatesManager.onBindViewHolder(holder, getList(), position, collectionPosition, payloads)
}
final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) = Unit
/**
* Adds [AdapterDelegate] to adapter.
*
* @param delegate Delegate to add.
*/
fun addDelegate(delegate: AdapterDelegate<*>) = delegatesManager.addDelegate(delegate)
/**
* Removes [AdapterDelegate] from adapter.
*
* @param delegate Delegate to remove.
*/
fun removeDelegate(delegate: AdapterDelegate<*>) = delegatesManager.removeDelegate(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<TItem>) = differ.submitList(list)
/**
* Get the current List - any diffing to present this list has already been computed and
* dispatched via the ListUpdateCallback.
* <p>
* If a <code>null</code> List, or no List has been submitted, an empty list will be returned.
* <p>
* The returned list may not be mutated - mutations to content must be done through
* {@link #submitList(List)}.
*
* @return current List.
*/
fun getList(): List<TItem> = differ.currentList
fun getCollectionPosition(adapterPosition: Int) = adapterPosition - getHeadersCount()
}

View File

@ -0,0 +1,85 @@
package ru.touchin.roboswag.components.adapters;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import java.util.List;
/**
* Objects of such class controls creation and binding of specific type of RecyclerView's ViewHolders.
* Such delegates are creating and binding ViewHolders for specific items.
* Default {@link #getItemViewType} is generating on construction of object.
*
* @param <TViewHolder> Type of {@link RecyclerView.ViewHolder} of delegate;
* @param <TItem> Type of items to bind to {@link RecyclerView.ViewHolder}s.
*/
public abstract class ItemAdapterDelegate<TViewHolder extends RecyclerView.ViewHolder, TItem> extends AdapterDelegate<TViewHolder> {
@Override
public boolean isForViewType(@NonNull final List<Object> 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, List)}.
*
* @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 boolean isForViewType(@NonNull final Object item, final int adapterPosition, final int collectionPosition) {
return true;
}
@Override
public long getItemId(@NonNull final List<Object> items, final int adapterPosition, final int collectionPosition) {
//noinspection unchecked
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 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 adapterPosition, final int collectionPosition) {
return 0;
}
@Override
public void onBindViewHolder(
@NonNull final RecyclerView.ViewHolder holder,
@NonNull final List<Object> items,
final int adapterPosition,
final int collectionPosition,
@NonNull final List<Object> 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 in adapter;
* @param adapterPosition Position of item in adapter;
* @param collectionPosition Position of item in collection that contains item;
* @param payloads Payloads;
*/
public abstract void onBindViewHolder(
@NonNull final TViewHolder holder,
@NonNull final TItem item,
final int adapterPosition,
final int collectionPosition,
@NonNull final List<Object> payloads
);
}

View File

@ -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.notifyItemRangeInserted(position + offsetProvider(), count)
}
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)
}
}

View File

@ -0,0 +1,68 @@
package ru.touchin.roboswag.components.adapters;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import java.util.List;
/**
* Objects of such class controls creation and binding of specific type of RecyclerView's ViewHolders.
* Such delegates are creating and binding ViewHolders by position in adapter.
* Default {@link #getItemViewType} is generating on construction of object.
*
* @param <TViewHolder> Type of {@link RecyclerView.ViewHolder} of delegate.
*/
public abstract class PositionAdapterDelegate<TViewHolder extends RecyclerView.ViewHolder> extends AdapterDelegate<TViewHolder> {
@Override
public boolean isForViewType(@NonNull final List<Object> items, final int adapterPosition, final int collectionPosition) {
return isForViewType(adapterPosition);
}
/**
* Returns if object is processable by this delegate.
*
* @param adapterPosition Position of item in adapter;
* @return True if item is processable by this delegate.
*/
public abstract boolean isForViewType(final int adapterPosition);
@Override
public long getItemId(@NonNull final List<Object> 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 adapterPosition Position of item in adapter;
* @return Unique item ID.
*/
public long getItemId(final int adapterPosition) {
return 0;
}
@Override
public void onBindViewHolder(
@NonNull final RecyclerView.ViewHolder holder,
@NonNull final List<Object> items,
final int adapterPosition,
final int collectionPosition,
@NonNull final List<Object> payloads
) {
//noinspection unchecked
onBindViewHolder((TViewHolder) holder, adapterPosition, payloads);
}
/**
* Binds position with payloads to ViewHolder.
*
* @param holder ViewHolder to bind position to;
* @param adapterPosition Position of item in adapter;
* @param payloads Payloads.
*/
public void onBindViewHolder(@NonNull final TViewHolder holder, final int adapterPosition, @NonNull final List<Object> payloads) {
//do nothing by default
}
}

View File

@ -14,8 +14,8 @@ android {
}
dependencies {
api project(":components-utils")
api project(":components-logging")
api project(":utils")
api project(":logging")
implementation "com.android.support:support-annotations:$versions.supportLibrary"

View File

@ -14,8 +14,8 @@ android {
}
dependencies {
api project(":components-utils")
api project(":components-logging")
api project(":utils")
api project(":logging")
implementation "com.android.support:design:$versions.supportLibrary"
}