aboutsummaryrefslogtreecommitdiff
path: root/fastadapter/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'fastadapter/src/main')
-rw-r--r--fastadapter/src/main/AndroidManifest.xml1
-rw-r--r--fastadapter/src/main/kotlin/ca/allanwang/kau/adapters/AdapterUtils.kt44
-rw-r--r--fastadapter/src/main/kotlin/ca/allanwang/kau/adapters/FastItemThemedAdapter.kt201
-rw-r--r--fastadapter/src/main/kotlin/ca/allanwang/kau/adapters/RepeatedClickListener.kt70
-rw-r--r--fastadapter/src/main/kotlin/ca/allanwang/kau/animators/AnimatorInterfaces.kt50
-rw-r--r--fastadapter/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java765
-rw-r--r--fastadapter/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt77
-rw-r--r--fastadapter/src/main/kotlin/ca/allanwang/kau/animators/FadeScaleAnimator.kt73
-rw-r--r--fastadapter/src/main/kotlin/ca/allanwang/kau/animators/KauAnimator.kt89
-rw-r--r--fastadapter/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt55
-rw-r--r--fastadapter/src/main/kotlin/ca/allanwang/kau/animators/SlideAnimator.kt83
-rw-r--r--fastadapter/src/main/kotlin/ca/allanwang/kau/iitems/CardIItem.kt140
-rw-r--r--fastadapter/src/main/kotlin/ca/allanwang/kau/iitems/HeaderIItem.kt59
-rw-r--r--fastadapter/src/main/kotlin/ca/allanwang/kau/iitems/KauIItem.kt42
-rw-r--r--fastadapter/src/main/res/layout/kau_iitem_card.xml74
-rw-r--r--fastadapter/src/main/res/layout/kau_iitem_header.xml16
16 files changed, 1839 insertions, 0 deletions
diff --git a/fastadapter/src/main/AndroidManifest.xml b/fastadapter/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ab8cebb
--- /dev/null
+++ b/fastadapter/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="ca.allanwang.kau.fastadapter" />
diff --git a/fastadapter/src/main/kotlin/ca/allanwang/kau/adapters/AdapterUtils.kt b/fastadapter/src/main/kotlin/ca/allanwang/kau/adapters/AdapterUtils.kt
new file mode 100644
index 0000000..17fd09f
--- /dev/null
+++ b/fastadapter/src/main/kotlin/ca/allanwang/kau/adapters/AdapterUtils.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * 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 ca.allanwang.kau.adapters
+
+import com.mikepenz.fastadapter.FastAdapter
+import com.mikepenz.fastadapter.IAdapter
+import com.mikepenz.fastadapter.IAdapterExtension
+import com.mikepenz.fastadapter.IItem
+import com.mikepenz.fastadapter.select.SelectExtension
+
+/**
+ * Created by Allan Wang on 2017-11-08.
+ */
+
+/**
+ * Add kotlin's generic syntax to better support out types
+ */
+fun <Item : IItem<*, *>> fastAdapter(vararg adapter: IAdapter<out Item>) =
+ FastAdapter.with<Item, IAdapter<out Item>>(adapter.toList())!!
+
+inline fun <reified T : IAdapterExtension<Item>, Item : IItem<*, *>> FastAdapter<Item>.getExtension(): T? =
+ getExtension(T::class.java)
+
+/**
+ * Returns selection size, or -1 if selection is disabled
+ */
+inline val <Item : IItem<*, *>> IAdapter<Item>.selectionSize: Int
+ get() = fastAdapter.getExtension<SelectExtension<Item>, Item>()?.selections?.size ?: -1
+
+inline val <Item : IItem<*, *>> IAdapter<Item>.selectedItems: Set<Item>
+ get() = fastAdapter.getExtension<SelectExtension<Item>, Item>()?.selectedItems ?: emptySet()
diff --git a/fastadapter/src/main/kotlin/ca/allanwang/kau/adapters/FastItemThemedAdapter.kt b/fastadapter/src/main/kotlin/ca/allanwang/kau/adapters/FastItemThemedAdapter.kt
new file mode 100644
index 0000000..152982f
--- /dev/null
+++ b/fastadapter/src/main/kotlin/ca/allanwang/kau/adapters/FastItemThemedAdapter.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * 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 ca.allanwang.kau.adapters
+
+import android.content.res.ColorStateList
+import android.os.Build
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.RequiresApi
+import ca.allanwang.kau.ui.createSimpleRippleDrawable
+import ca.allanwang.kau.utils.adjustAlpha
+import com.mikepenz.fastadapter.IItem
+import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter
+
+/**
+ * Created by Allan Wang on 2017-06-29.
+ *
+ * Adapter with a set of colors that will be added to all subsequent items
+ * Changing a color while the adapter is not empty will reload all items
+ *
+ * This adapter overrides every method where an item is added
+ * If that item extends [ThemableIItem], then the colors will be set
+ */
+class FastItemThemedAdapter<Item : IItem<*, *>>(
+ textColor: Int? = null,
+ backgroundColor: Int? = null,
+ accentColor: Int? = null
+) : FastItemAdapter<Item>() {
+ constructor(colors: ThemableIItemColors) : this(colors.textColor, colors.backgroundColor, colors.accentColor)
+
+ var textColor: Int? = textColor
+ set(value) {
+ if (field == value) return
+ field = value
+ themeChanged()
+ }
+ var backgroundColor: Int? = backgroundColor
+ set(value) {
+ if (field == value) return
+ field = value
+ themeChanged()
+ }
+ var accentColor: Int? = accentColor
+ set(value) {
+ if (field == value) return
+ field = value
+ themeChanged()
+ }
+
+ fun setColors(colors: ThemableIItemColors) {
+ this.textColor = colors.textColor
+ this.backgroundColor = colors.backgroundColor
+ this.accentColor = colors.accentColor
+ }
+
+ fun themeChanged() {
+ if (adapterItemCount == 0) return
+ injectTheme(adapterItems)
+ notifyAdapterDataSetChanged()
+ }
+
+ override fun add(position: Int, items: List<Item>): FastItemAdapter<Item> {
+ injectTheme(items)
+ return super.add(position, items)
+ }
+
+ override fun add(position: Int, item: Item): FastItemAdapter<Item> {
+ injectTheme(item)
+ return super.add(position, item)
+ }
+
+ override fun add(item: Item): FastItemAdapter<Item> {
+ injectTheme(item)
+ return super.add(item)
+ }
+
+ override fun add(items: List<Item>?): FastItemAdapter<Item> {
+ injectTheme(items)
+ return super.add(items)
+ }
+
+ override fun set(items: List<Item>?): FastItemAdapter<Item> {
+ injectTheme(items)
+ return super.set(items)
+ }
+
+ override fun set(position: Int, item: Item): FastItemAdapter<Item> {
+ injectTheme(item)
+ return super.set(position, item)
+ }
+
+ override fun setNewList(items: List<Item>?, retainFilter: Boolean): FastItemAdapter<Item> {
+ injectTheme(items)
+ return super.setNewList(items, retainFilter)
+ }
+
+ override fun setNewList(items: List<Item>?): FastItemAdapter<Item> {
+ injectTheme(items)
+ return super.setNewList(items)
+ }
+
+ private fun injectTheme(items: Collection<IItem<*, *>?>?) {
+ items?.forEach { injectTheme(it) }
+ }
+
+ protected fun injectTheme(item: IItem<*, *>?) {
+ if (item is ThemableIItem && item.themeEnabled) {
+ item.textColor = textColor
+ item.backgroundColor = backgroundColor
+ item.accentColor = accentColor
+ }
+ }
+}
+
+interface ThemableIItemColors {
+ var textColor: Int?
+ var backgroundColor: Int?
+ var accentColor: Int?
+}
+
+class ThemableIItemColorsDelegate : ThemableIItemColors {
+ override var textColor: Int? = null
+ override var backgroundColor: Int? = null
+ override var accentColor: Int? = null
+}
+
+/**
+ * Interface that needs to be implemented by every iitem
+ * Holds the color values and has helper methods to inject the colors
+ */
+interface ThemableIItem : ThemableIItemColors {
+ var themeEnabled: Boolean
+ fun bindTextColor(vararg views: TextView?)
+ fun bindTextColorSecondary(vararg views: TextView?)
+ fun bindDividerColor(vararg views: View?)
+ fun bindAccentColor(vararg views: TextView?)
+ fun bindBackgroundColor(vararg views: View?)
+ fun bindBackgroundRipple(vararg views: View?)
+ fun bindIconColor(vararg views: ImageView?)
+}
+
+/**
+ * The delegate for [ThemableIItem]
+ */
+class ThemableIItemDelegate : ThemableIItem, ThemableIItemColors by ThemableIItemColorsDelegate() {
+ override var themeEnabled: Boolean = true
+
+ override fun bindTextColor(vararg views: TextView?) {
+ val color = textColor ?: return
+ views.forEach { it?.setTextColor(color) }
+ }
+
+ override fun bindTextColorSecondary(vararg views: TextView?) {
+ val color = textColor?.adjustAlpha(0.8f) ?: return
+ views.forEach { it?.setTextColor(color) }
+ }
+
+ override fun bindAccentColor(vararg views: TextView?) {
+ val color = accentColor ?: textColor ?: return
+ views.forEach { it?.setTextColor(color) }
+ }
+
+ override fun bindDividerColor(vararg views: View?) {
+ val color = (textColor ?: accentColor)?.adjustAlpha(0.1f) ?: return
+ views.forEach { it?.setBackgroundColor(color) }
+ }
+
+ override fun bindBackgroundColor(vararg views: View?) {
+ val color = backgroundColor ?: return
+ views.forEach { it?.setBackgroundColor(color) }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun bindBackgroundRipple(vararg views: View?) {
+ val background = backgroundColor ?: return
+ val foreground = accentColor ?: textColor ?: backgroundColor
+ ?: return //default to normal background
+ val ripple = createSimpleRippleDrawable(foreground, background)
+ views.forEach { it?.background = ripple }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override fun bindIconColor(vararg views: ImageView?) {
+ val color = accentColor ?: textColor ?: return
+ views.forEach { it?.drawable?.setTintList(ColorStateList.valueOf(color)) }
+ }
+}
diff --git a/fastadapter/src/main/kotlin/ca/allanwang/kau/adapters/RepeatedClickListener.kt b/fastadapter/src/main/kotlin/ca/allanwang/kau/adapters/RepeatedClickListener.kt
new file mode 100644
index 0000000..40b4774
--- /dev/null
+++ b/fastadapter/src/main/kotlin/ca/allanwang/kau/adapters/RepeatedClickListener.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * 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 ca.allanwang.kau.adapters
+
+import android.view.View
+import androidx.annotation.IntRange
+import com.mikepenz.fastadapter.FastAdapter
+import com.mikepenz.fastadapter.IAdapter
+import com.mikepenz.fastadapter.IItem
+import com.mikepenz.fastadapter.listeners.OnClickListener
+
+/**
+ * Created by Allan Wang on 26/12/17.
+ */
+fun <Item : IItem<*, *>> FastAdapter<Item>.withOnRepeatedClickListener(
+ count: Int,
+ duration: Long,
+ event: OnClickListener<Item>
+) =
+ withOnClickListener(RepeatedClickListener(count, duration, event))
+
+/**
+ * Registers and skips each click until the designated [count] clicks are triggered,
+ * each within [duration] from each other.
+ * Only then will the [event] be fired, and everything will be reset.
+ */
+private class RepeatedClickListener<Item : IItem<*, *>>(
+ @IntRange(from = 1) val count: Int,
+ @IntRange(from = 1) val duration: Long,
+ val event: OnClickListener<Item>
+) : OnClickListener<Item> {
+
+ init {
+ if (count <= 0)
+ throw IllegalArgumentException("RepeatedClickListener's count must be > 1")
+ if (duration <= 0)
+ throw IllegalArgumentException("RepeatedClickListener's duration must be > 1L")
+ }
+
+ private var chain = 0
+ private var time = -1L
+
+ override fun onClick(v: View?, adapter: IAdapter<Item>, item: Item, position: Int): Boolean {
+ val now = System.currentTimeMillis()
+ if (time - now < duration)
+ chain++
+ else
+ chain = 1
+ time = now
+ if (chain == count) {
+ chain = 0
+ event.onClick(v, adapter, item, position)
+ return true
+ }
+ return false
+ }
+}
diff --git a/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/AnimatorInterfaces.kt b/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/AnimatorInterfaces.kt
new file mode 100644
index 0000000..b83a2d0
--- /dev/null
+++ b/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/AnimatorInterfaces.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * 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 ca.allanwang.kau.animators
+
+import android.view.View
+import android.view.ViewPropertyAnimator
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * Created by Allan Wang on 2017-07-11.
+ */
+class KauAnimatorException(message: String) : RuntimeException(message)
+
+interface KauAnimatorAdd {
+ fun animationPrepare(holder: RecyclerView.ViewHolder): View.() -> Unit
+ fun animation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator.() -> Unit
+ fun animationCleanup(holder: RecyclerView.ViewHolder): View.() -> Unit
+ fun getDelay(remove: Long, move: Long, change: Long): Long
+ var itemDelayFactor: Float
+}
+
+interface KauAnimatorRemove {
+ fun animation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator.() -> Unit
+ fun animationCleanup(holder: RecyclerView.ViewHolder): View.() -> Unit
+ fun getDelay(remove: Long, move: Long, change: Long): Long
+ var itemDelayFactor: Float
+}
+
+interface KauAnimatorChange {
+ fun changeOldAnimation(
+ holder: RecyclerView.ViewHolder,
+ changeInfo: BaseItemAnimator.ChangeInfo
+ ): ViewPropertyAnimator.() -> Unit
+
+ fun changeNewAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator.() -> Unit
+ fun changeAnimationCleanup(holder: RecyclerView.ViewHolder): View.() -> Unit
+}
diff --git a/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java b/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java
new file mode 100644
index 0000000..4ead735
--- /dev/null
+++ b/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java
@@ -0,0 +1,765 @@
+package ca.allanwang.kau.animators;
+
+/*
+ * Created by Allan Wang on 2017-06-27.
+ *
+ * Based on Item Animator by {@author Mike Penz}
+ * Rewritten to match with the updated compat dependencies
+ */
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * 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.
+ */
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import androidx.annotation.NonNull;
+import androidx.core.view.ViewCompat;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+import androidx.recyclerview.widget.SimpleItemAnimator;
+
+import android.view.View;
+import android.view.ViewPropertyAnimator;
+import android.view.animation.Interpolator;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This implementation of {@link androidx.recyclerview.widget.RecyclerView.ItemAnimator} provides basic
+ * animations on remove, add, and move events that happen to the items in
+ * a RecyclerView. RecyclerView uses a DefaultItemAnimator by default.
+ *
+ * @see androidx.recyclerview.widget.RecyclerView#setItemAnimator(androidx.recyclerview.widget.RecyclerView.ItemAnimator)
+ */
+public abstract class BaseItemAnimator extends SimpleItemAnimator {
+ private static final boolean DEBUG = false;
+
+ private static TimeInterpolator sDefaultInterpolator;
+
+ private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<>();
+ private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>();
+ private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
+ private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
+
+ ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>();
+ ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
+ ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();
+
+ ArrayList<ViewHolder> mAddAnimations = new ArrayList<>();
+ ArrayList<ViewHolder> mMoveAnimations = new ArrayList<>();
+ ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>();
+ ArrayList<ViewHolder> mChangeAnimations = new ArrayList<>();
+
+ public Interpolator interpolator;
+
+ private static class MoveInfo {
+ public ViewHolder holder;
+ public int fromX, fromY, toX, toY;
+
+ MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
+ this.holder = holder;
+ this.fromX = fromX;
+ this.fromY = fromY;
+ this.toX = toX;
+ this.toY = toY;
+ }
+ }
+
+ public static class ChangeInfo {
+ public ViewHolder oldHolder, newHolder;
+ public int fromX, fromY, toX, toY;
+
+ private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) {
+ this.oldHolder = oldHolder;
+ this.newHolder = newHolder;
+ }
+
+ ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder,
+ int fromX, int fromY, int toX, int toY) {
+ this(oldHolder, newHolder);
+ this.fromX = fromX;
+ this.fromY = fromY;
+ this.toX = toX;
+ this.toY = toY;
+ }
+
+ @Override
+ public String toString() {
+ return "ChangeInfo{"
+ + "oldHolder=" + oldHolder
+ + ", newHolder=" + newHolder
+ + ", fromX=" + fromX
+ + ", fromY=" + fromY
+ + ", toX=" + toX
+ + ", toY=" + toY
+ + '}';
+ }
+ }
+
+ @Override
+ public void runPendingAnimations() {
+ boolean removalsPending = !mPendingRemovals.isEmpty();
+ boolean movesPending = !mPendingMoves.isEmpty();
+ boolean changesPending = !mPendingChanges.isEmpty();
+ boolean additionsPending = !mPendingAdditions.isEmpty();
+ if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
+ // nothing to animate
+ return;
+ }
+ // First, remove stuff
+ for (ViewHolder holder : mPendingRemovals) {
+ animateRemoveImpl(holder);
+ }
+ mPendingRemovals.clear();
+ // Next, move stuff
+ if (movesPending) {
+ final ArrayList<MoveInfo> moves = new ArrayList<>();
+ moves.addAll(mPendingMoves);
+ mMovesList.add(moves);
+ mPendingMoves.clear();
+ Runnable mover = new Runnable() {
+ @Override
+ public void run() {
+ for (MoveInfo moveInfo : moves) {
+ animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
+ moveInfo.toX, moveInfo.toY);
+ }
+ moves.clear();
+ mMovesList.remove(moves);
+ }
+ };
+ if (removalsPending) {
+ View view = moves.get(0).holder.itemView;
+ ViewCompat.postOnAnimationDelayed(view, mover, 0);
+ } else {
+ mover.run();
+ }
+ }
+ // Next, change stuff, to run in parallel with move animations
+ if (changesPending) {
+ final ArrayList<ChangeInfo> changes = new ArrayList<>();
+ changes.addAll(mPendingChanges);
+ mChangesList.add(changes);
+ mPendingChanges.clear();
+ Runnable changer = new Runnable() {
+ @Override
+ public void run() {
+ for (ChangeInfo change : changes) {
+ animateChangeImpl(change);
+ }
+ changes.clear();
+ mChangesList.remove(changes);
+ }
+ };
+ if (removalsPending) {
+ ViewHolder holder = changes.get(0).oldHolder;
+ long moveDuration = movesPending ? getMoveDuration() : 0;
+ ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDelay(getRemoveDuration(), moveDuration, getChangeDuration()));
+ } else {
+ changer.run();
+ }
+ }
+ // Next, add stuff
+ if (additionsPending) {
+ final ArrayList<ViewHolder> additions = new ArrayList<>();
+ additions.addAll(mPendingAdditions);
+ mAdditionsList.add(additions);
+ mPendingAdditions.clear();
+ Runnable adder = new Runnable() {
+ @Override
+ public void run() {
+ for (ViewHolder holder : additions) {
+ animateAddImpl(holder);
+ }
+ additions.clear();
+ mAdditionsList.remove(additions);
+ }
+ };
+ if (removalsPending || movesPending || changesPending) {
+ long removeDuration = removalsPending ? getRemoveDuration() : 0;
+ long moveDuration = movesPending ? getMoveDuration() : 0;
+ long changeDuration = changesPending ? getChangeDuration() : 0;
+ View view = additions.get(0).itemView;
+ ViewCompat.postOnAnimationDelayed(view, adder, getAddDelay(removeDuration, moveDuration, changeDuration));
+ } else {
+ adder.run();
+ }
+ }
+ }
+
+ /**
+ * used to calculated the delay until the remove animation should start
+ *
+ * @param remove the remove duration
+ * @param move the move duration
+ * @param change the change duration
+ * @return the calculated delay for the remove items animation
+ */
+ public long getRemoveDelay(long remove, long move, long change) {
+ return remove + Math.max(move, change);
+ }
+
+ /**
+ * used to calculated the delay until the add animation should start
+ *
+ * @param remove the remove duration
+ * @param move the move duration
+ * @param change the change duration
+ * @return the calculated delay for the add items animation
+ */
+ public long getAddDelay(long remove, long move, long change) {
+ return remove + Math.max(move, change);
+ }
+
+ @Override
+ public boolean animateRemove(final ViewHolder holder) {
+ resetAnimation(holder);
+ mPendingRemovals.add(holder);
+ return true;
+ }
+
+ private void animateRemoveImpl(final ViewHolder holder) {
+ final ViewPropertyAnimator animation = removeAnimation(holder);
+ mRemoveAnimations.add(holder);
+ animation.setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchRemoveStarting(holder);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ animation.setListener(null);
+ removeAnimationCleanup(holder);
+ dispatchRemoveFinished(holder);
+ mRemoveAnimations.remove(holder);
+ dispatchFinishedWhenDone();
+ }
+ }).start();
+ }
+
+ abstract public ViewPropertyAnimator removeAnimation(ViewHolder holder);
+
+ abstract public void removeAnimationCleanup(ViewHolder holder);
+
+ @Override
+ public boolean animateAdd(final ViewHolder holder) {
+ resetAnimation(holder);
+ addAnimationPrepare(holder);
+ mPendingAdditions.add(holder);
+ return true;
+ }
+
+ void animateAddImpl(final ViewHolder holder) {
+ final ViewPropertyAnimator animation = addAnimation(holder);
+ mAddAnimations.add(holder);
+ animation.setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchAddStarting(holder);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+ addAnimationCleanup(holder);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ animation.setListener(null);
+ dispatchAddFinished(holder);
+ mAddAnimations.remove(holder);
+ dispatchFinishedWhenDone();
+ addAnimationCleanup(holder);
+ }
+ }).start();
+ }
+
+ /**
+ * the animation to prepare the view before the add animation is run
+ *
+ * @param holder
+ */
+ public abstract void addAnimationPrepare(ViewHolder holder);
+
+ /**
+ * the animation for adding a view
+ *
+ * @param holder
+ * @return
+ */
+ public abstract ViewPropertyAnimator addAnimation(ViewHolder holder);
+
+ /**
+ * the cleanup method if the animation needs to be stopped. and tro prepare for the next view
+ *
+ * @param holder
+ */
+ abstract void addAnimationCleanup(ViewHolder holder);
+
+ @Override
+ public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
+ int toX, int toY) {
+ final View view = holder.itemView;
+ fromX += (int) holder.itemView.getTranslationX();
+ fromY += (int) holder.itemView.getTranslationY();
+ resetAnimation(holder);
+ int deltaX = toX - fromX;
+ int deltaY = toY - fromY;
+ if (deltaX == 0 && deltaY == 0) {
+ dispatchMoveFinished(holder);
+ return false;
+ }
+ if (deltaX != 0) {
+ view.setTranslationX(-deltaX);
+ }
+ if (deltaY != 0) {
+ view.setTranslationY(-deltaY);
+ }
+ mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
+ return true;
+ }
+
+ void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
+ final View view = holder.itemView;
+ final int deltaX = toX - fromX;
+ final int deltaY = toY - fromY;
+ if (deltaX != 0) {
+ view.animate().translationX(0);
+ }
+ if (deltaY != 0) {
+ view.animate().translationY(0);
+ }
+ // TODO: make EndActions end listeners instead, since end actions aren't called when
+ // vpas are canceled (and can't end them. why?)
+ // need listener functionality in VPACompat for this. Ick.
+ final ViewPropertyAnimator animation = view.animate();
+ mMoveAnimations.add(holder);
+ animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchMoveStarting(holder);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+ if (deltaX != 0) {
+ view.setTranslationX(0);
+ }
+ if (deltaY != 0) {
+ view.setTranslationY(0);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ animation.setListener(null);
+ dispatchMoveFinished(holder);
+ mMoveAnimations.remove(holder);
+ dispatchFinishedWhenDone();
+ }
+ }).start();
+ }
+
+ @Override
+ public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
+ int fromX, int fromY, int toX, int toY) {
+ if (oldHolder == newHolder) {
+ // Don't know how to run change animations when the same view holder is re-used.
+ // run a move animation to handle position changes.
+ return animateMove(oldHolder, fromX, fromY, toX, toY);
+ }
+ changeAnimation(oldHolder, newHolder,
+ fromX, fromY, toX, toY);
+ mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
+ return true;
+ }
+
+ void animateChangeImpl(final ChangeInfo changeInfo) {
+ final ViewHolder holder = changeInfo.oldHolder;
+ final View view = holder == null ? null : holder.itemView;
+ final ViewHolder newHolder = changeInfo.newHolder;
+ final View newView = newHolder != null ? newHolder.itemView : null;
+ if (view != null) {
+ final ViewPropertyAnimator oldViewAnim = changeOldAnimation(holder, changeInfo);
+ mChangeAnimations.add(changeInfo.oldHolder);
+ oldViewAnim.setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchChangeStarting(changeInfo.oldHolder, true);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ oldViewAnim.setListener(null);
+ changeAnimationCleanup(holder);
+ view.setTranslationX(0);
+ view.setTranslationY(0);
+ dispatchChangeFinished(changeInfo.oldHolder, true);
+ mChangeAnimations.remove(changeInfo.oldHolder);
+ dispatchFinishedWhenDone();
+ }
+ }).start();
+ }
+ if (newView != null) {
+ final ViewPropertyAnimator newViewAnimation = changeNewAnimation(newHolder);
+ mChangeAnimations.add(changeInfo.newHolder);
+ newViewAnimation.setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchChangeStarting(changeInfo.newHolder, false);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ newViewAnimation.setListener(null);
+ changeAnimationCleanup(newHolder);
+ newView.setTranslationX(0);
+ newView.setTranslationY(0);
+ dispatchChangeFinished(changeInfo.newHolder, false);
+ mChangeAnimations.remove(changeInfo.newHolder);
+ dispatchFinishedWhenDone();
+ }
+ }).start();
+ }
+ }
+
+ /**
+ * the whole change animation if we have to cross animate two views
+ *
+ * @param oldHolder
+ * @param newHolder
+ * @param fromX
+ * @param fromY
+ * @param toX
+ * @param toY
+ */
+ public void changeAnimation(ViewHolder oldHolder, ViewHolder newHolder, int fromX, int fromY, int toX, int toY) {
+ final float prevTranslationX = oldHolder.itemView.getTranslationX();
+ final float prevTranslationY = oldHolder.itemView.getTranslationY();
+ final float prevValue = oldHolder.itemView.getAlpha();
+ resetAnimation(oldHolder);
+ int deltaX = (int) (toX - fromX - prevTranslationX);
+ int deltaY = (int) (toY - fromY - prevTranslationY);
+ // recover prev translation state after ending animation
+ oldHolder.itemView.setTranslationX(prevTranslationX);
+ oldHolder.itemView.setTranslationY(prevTranslationY);
+
+ oldHolder.itemView.setAlpha(prevValue);
+ if (newHolder != null) {
+ // carry over translation values
+ resetAnimation(newHolder);
+ newHolder.itemView.setTranslationX(-deltaX);
+ newHolder.itemView.setTranslationY(-deltaY);
+ newHolder.itemView.setAlpha(0);
+ }
+ }
+
+ /**
+ * the animation for removing the old view
+ *
+ * @param holder
+ * @return
+ */
+ public abstract ViewPropertyAnimator changeOldAnimation(ViewHolder holder, ChangeInfo changeInfo);
+
+ /**
+ * the animation for changing the new view
+ *
+ * @param holder
+ * @return
+ */
+ public abstract ViewPropertyAnimator changeNewAnimation(ViewHolder holder);
+
+ /**
+ * the cleanup method if the animation needs to be stopped. and to prepare for the next view
+ *
+ * @param holder
+ */
+ public abstract void changeAnimationCleanup(ViewHolder holder);
+
+ private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) {
+ for (int i = infoList.size() - 1; i >= 0; i--) {
+ ChangeInfo changeInfo = infoList.get(i);
+ if (endChangeAnimationIfNecessary(changeInfo, item)) {
+ if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
+ infoList.remove(changeInfo);
+ }
+ }
+ }
+ }
+
+ private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
+ if (changeInfo.oldHolder != null) {
+ endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
+ }
+ if (changeInfo.newHolder != null) {
+ endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
+ }
+ }
+
+ private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) {
+ boolean oldItem = false;
+ if (changeInfo.newHolder == item) {
+ changeInfo.newHolder = null;
+ } else if (changeInfo.oldHolder == item) {
+ changeInfo.oldHolder = null;
+ oldItem = true;
+ } else {
+ return false;
+ }
+ changeAnimationCleanup(item);
+ item.itemView.setTranslationX(0);
+ item.itemView.setTranslationY(0);
+ dispatchChangeFinished(item, oldItem);
+ return true;
+ }
+
+ @Override
+ public void endAnimation(ViewHolder item) {
+ final View view = item.itemView;
+ // this will trigger end callback which should set properties to their target values.
+ view.animate().cancel();
+ // TODO if some other animations are chained to end, how do we cancel them as well?
+ for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
+ MoveInfo moveInfo = mPendingMoves.get(i);
+ if (moveInfo.holder == item) {
+ view.setTranslationY(0);
+ view.setTranslationX(0);
+ dispatchMoveFinished(item);
+ mPendingMoves.remove(i);
+ }
+ }
+ endChangeAnimation(mPendingChanges, item);
+ if (mPendingRemovals.remove(item)) {
+ removeAnimationCleanup(item);
+ dispatchRemoveFinished(item);
+ }
+ if (mPendingAdditions.remove(item)) {
+ addAnimationCleanup(item);
+ dispatchAddFinished(item);
+ }
+
+ for (int i = mChangesList.size() - 1; i >= 0; i--) {
+ ArrayList<ChangeInfo> changes = mChangesList.get(i);
+ endChangeAnimation(changes, item);
+ if (changes.isEmpty()) {
+ mChangesList.remove(i);
+ }
+ }
+ for (int i = mMovesList.size() - 1; i >= 0; i--) {
+ ArrayList<MoveInfo> moves = mMovesList.get(i);
+ for (int j = moves.size() - 1; j >= 0; j--) {
+ MoveInfo moveInfo = moves.get(j);
+ if (moveInfo.holder == item) {
+ view.setTranslationY(0);
+ view.setTranslationX(0);
+ dispatchMoveFinished(item);
+ moves.remove(j);
+ if (moves.isEmpty()) {
+ mMovesList.remove(i);
+ }
+ break;
+ }
+ }
+ }
+ for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
+ ArrayList<ViewHolder> additions = mAdditionsList.get(i);
+ if (additions.remove(item)) {
+ addAnimationCleanup(item);
+ dispatchAddFinished(item);
+ if (additions.isEmpty()) {
+ mAdditionsList.remove(i);
+ }
+ }
+ }
+
+ // animations should be ended by the cancel above.
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (mRemoveAnimations.remove(item) && DEBUG) {
+ throw new IllegalStateException("after animation is cancelled, item should not be in "
+ + "mRemoveAnimations list");
+ }
+
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (mAddAnimations.remove(item) && DEBUG) {
+ throw new IllegalStateException("after animation is cancelled, item should not be in "
+ + "mAddAnimations list");
+ }
+
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (mChangeAnimations.remove(item) && DEBUG) {
+ throw new IllegalStateException("after animation is cancelled, item should not be in "
+ + "mChangeAnimations list");
+ }
+
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (mMoveAnimations.remove(item) && DEBUG) {
+ throw new IllegalStateException("after animation is cancelled, item should not be in "
+ + "mMoveAnimations list");
+ }
+ dispatchFinishedWhenDone();
+ }
+
+ private void resetAnimation(ViewHolder holder) {
+
+ if (sDefaultInterpolator == null) {
+ sDefaultInterpolator = new ValueAnimator().getInterpolator();
+ }
+ holder.itemView.animate().setInterpolator(sDefaultInterpolator);
+ endAnimation(holder);
+ }
+
+ @Override
+ public boolean isRunning() {
+ return (!mPendingAdditions.isEmpty()
+ || !mPendingChanges.isEmpty()
+ || !mPendingMoves.isEmpty()
+ || !mPendingRemovals.isEmpty()
+ || !mMoveAnimations.isEmpty()
+ || !mRemoveAnimations.isEmpty()
+ || !mAddAnimations.isEmpty()
+ || !mChangeAnimations.isEmpty()
+ || !mMovesList.isEmpty()
+ || !mAdditionsList.isEmpty()
+ || !mChangesList.isEmpty());
+ }
+
+ /**
+ * Check the state of currently pending and running animations. If there are none
+ * pending/running, call {@link #dispatchAnimationsFinished()} to notify any
+ * listeners.
+ */
+ void dispatchFinishedWhenDone() {
+ if (!isRunning()) {
+ dispatchAnimationsFinished();
+ }
+ }
+
+ @Override
+ public void endAnimations() {
+ int count = mPendingMoves.size();
+ for (int i = count - 1; i >= 0; i--) {
+ MoveInfo item = mPendingMoves.get(i);
+ View view = item.holder.itemView;
+ view.setTranslationY(0);
+ view.setTranslationX(0);
+ dispatchMoveFinished(item.holder);
+ mPendingMoves.remove(i);
+ }
+ count = mPendingRemovals.size();
+ for (int i = count - 1; i >= 0; i--) {
+ ViewHolder item = mPendingRemovals.get(i);
+ dispatchRemoveFinished(item);
+ mPendingRemovals.remove(i);
+ }
+ count = mPendingAdditions.size();
+ for (int i = count - 1; i >= 0; i--) {
+ ViewHolder item = mPendingAdditions.get(i);
+ addAnimationCleanup(item);
+ dispatchAddFinished(item);
+ mPendingAdditions.remove(i);
+ }
+ count = mPendingChanges.size();
+ for (int i = count - 1; i >= 0; i--) {
+ endChangeAnimationIfNecessary(mPendingChanges.get(i));
+ }
+ mPendingChanges.clear();
+ if (!isRunning()) {
+ return;
+ }
+
+ int listCount = mMovesList.size();
+ for (int i = listCount - 1; i >= 0; i--) {
+ ArrayList<MoveInfo> moves = mMovesList.get(i);
+ count = moves.size();
+ for (int j = count - 1; j >= 0; j--) {
+ MoveInfo moveInfo = moves.get(j);
+ ViewHolder item = moveInfo.holder;
+ View view = item.itemView;
+ view.setTranslationY(0);
+ view.setTranslationX(0);
+ dispatchMoveFinished(moveInfo.holder);
+ moves.remove(j);
+ if (moves.isEmpty()) {
+ mMovesList.remove(moves);
+ }
+ }
+ }
+ listCount = mAdditionsList.size();
+ for (int i = listCount - 1; i >= 0; i--) {
+ ArrayList<ViewHolder> additions = mAdditionsList.get(i);
+ count = additions.size();
+ for (int j = count - 1; j >= 0; j--) {
+ ViewHolder item = additions.get(j);
+ View view = item.itemView;
+ addAnimationCleanup(item);
+ dispatchAddFinished(item);
+ additions.remove(j);
+ if (additions.isEmpty()) {
+ mAdditionsList.remove(additions);
+ }
+ }
+ }
+ listCount = mChangesList.size();
+ for (int i = listCount - 1; i >= 0; i--) {
+ ArrayList<ChangeInfo> changes = mChangesList.get(i);
+ count = changes.size();
+ for (int j = count - 1; j >= 0; j--) {
+ endChangeAnimationIfNecessary(changes.get(j));
+ if (changes.isEmpty()) {
+ mChangesList.remove(changes);
+ }
+ }
+ }
+
+ cancelAll(mRemoveAnimations);
+ cancelAll(mMoveAnimations);
+ cancelAll(mAddAnimations);
+ cancelAll(mChangeAnimations);
+
+ dispatchAnimationsFinished();
+ }
+
+ void cancelAll(List<ViewHolder> viewHolders) {
+ for (int i = viewHolders.size() - 1; i >= 0; i--) {
+ viewHolders.get(i).itemView.animate().cancel();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * If the payload list is not empty, DefaultItemAnimator returns <code>true</code>.
+ * When this is the case:
+ * <ul>
+ * <li>If you override {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}, both
+ * ViewHolder arguments will be the same instance.
+ * </li>
+ * <li>
+ * If you are not overriding {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)},
+ * then DefaultItemAnimator will call {@link #animateMove(ViewHolder, int, int, int, int)} and
+ * run a move animation instead.
+ * </li>
+ * </ul>
+ */
+ @Override
+ public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
+ @NonNull List<Object> payloads) {
+ return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
+ }
+}
diff --git a/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt b/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt
new file mode 100644
index 0000000..4e342ab
--- /dev/null
+++ b/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * 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 ca.allanwang.kau.animators
+
+import android.view.ViewPropertyAnimator
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * Created by Allan Wang on 2017-06-27.
+ */
+open class DefaultAnimator : BaseItemAnimator() {
+
+ override fun removeAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return holder.itemView.animate().apply {
+ alpha(0f)
+ duration = this@DefaultAnimator.removeDuration
+ interpolator = this@DefaultAnimator.interpolator
+ }
+ }
+
+ override fun removeAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ holder.itemView.alpha = 1f
+ }
+
+ override fun addAnimationPrepare(holder: RecyclerView.ViewHolder) {
+ holder.itemView.alpha = 0f
+ }
+
+ override fun addAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return holder.itemView.animate().apply {
+ alpha(1f)
+ duration = this@DefaultAnimator.addDuration
+ interpolator = this@DefaultAnimator.interpolator
+ }
+ }
+
+ override fun addAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ holder.itemView.alpha = 1f
+ }
+
+ override fun changeOldAnimation(holder: RecyclerView.ViewHolder, changeInfo: ChangeInfo): ViewPropertyAnimator {
+ return holder.itemView.animate().apply {
+ alpha(0f)
+ translationX(changeInfo.toX.toFloat() - changeInfo.fromX)
+ translationY(changeInfo.toY.toFloat() - changeInfo.fromY)
+ duration = this@DefaultAnimator.changeDuration
+ interpolator = this@DefaultAnimator.interpolator
+ }
+ }
+
+ override fun changeNewAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return holder.itemView.animate().apply {
+ alpha(1f)
+ translationX(0f)
+ translationY(0f)
+ duration = this@DefaultAnimator.changeDuration
+ interpolator = this@DefaultAnimator.interpolator
+ }
+ }
+
+ override fun changeAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ holder.itemView.alpha = 1f
+ }
+}
diff --git a/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/FadeScaleAnimator.kt b/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/FadeScaleAnimator.kt
new file mode 100644
index 0000000..9113b0e
--- /dev/null
+++ b/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/FadeScaleAnimator.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * 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 ca.allanwang.kau.animators
+
+import android.view.View
+import android.view.ViewPropertyAnimator
+import androidx.recyclerview.widget.RecyclerView
+import ca.allanwang.kau.utils.scaleXY
+
+/**
+ * Created by Allan Wang on 2017-07-11.
+ */
+class FadeScaleAnimatorAdd(val scaleFactor: Float = 1.0f, override var itemDelayFactor: Float = 0.125f) :
+ KauAnimatorAdd {
+
+ override fun animationPrepare(holder: RecyclerView.ViewHolder): View.() -> Unit = {
+ scaleXY = scaleFactor
+ alpha = 0f
+ }
+
+ override fun animation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator.() -> Unit = {
+ scaleXY(1f)
+ alpha(1f)
+ }
+
+ override fun animationCleanup(holder: RecyclerView.ViewHolder): View.() -> Unit = {
+ scaleXY = 1f
+ alpha = 1f
+ }
+
+ override fun getDelay(remove: Long, move: Long, change: Long): Long = 0L
+}
+
+class FadeScaleAnimatorRemove(val scaleFactor: Float = 1.0f, override var itemDelayFactor: Float = 0.125f) :
+ KauAnimatorRemove {
+
+ override fun animation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator.() -> Unit = {
+ scaleXY(scaleFactor)
+ alpha(0f)
+ }
+
+ override fun animationCleanup(holder: RecyclerView.ViewHolder): View.() -> Unit = {
+ scaleXY = 1f
+ alpha = 1f
+ }
+
+ override fun getDelay(remove: Long, move: Long, change: Long): Long = 0L
+}
+
+class FadeAnimatorChange : KauAnimatorChange {
+
+ override fun changeOldAnimation(
+ holder: RecyclerView.ViewHolder,
+ changeInfo: BaseItemAnimator.ChangeInfo
+ ): ViewPropertyAnimator.() -> Unit = { alpha(0f) }
+
+ override fun changeNewAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator.() -> Unit = { alpha(1f) }
+
+ override fun changeAnimationCleanup(holder: RecyclerView.ViewHolder): View.() -> Unit = { alpha = 1f }
+}
diff --git a/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/KauAnimator.kt b/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/KauAnimator.kt
new file mode 100644
index 0000000..b9df946
--- /dev/null
+++ b/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/KauAnimator.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * 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 ca.allanwang.kau.animators
+
+import android.view.ViewPropertyAnimator
+import androidx.recyclerview.widget.RecyclerView
+import ca.allanwang.kau.utils.KAU_BOTTOM
+import ca.allanwang.kau.utils.KAU_RIGHT
+
+/**
+ * Created by Allan Wang on 2017-06-27.
+ */
+open class KauAnimator(
+ val addAnimator: KauAnimatorAdd = SlideAnimatorAdd(KAU_BOTTOM),
+ val removeAnimator: KauAnimatorRemove = SlideAnimatorRemove(KAU_RIGHT),
+ val changeAnimator: KauAnimatorChange = FadeAnimatorChange()
+) : BaseItemAnimator() {
+
+ open fun startDelay(holder: RecyclerView.ViewHolder, duration: Long, factor: Float) =
+ Math.max(0L, (holder.adapterPosition * duration * factor).toLong())
+
+ override fun removeAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return holder.itemView.animate().apply {
+ startDelay = startDelay(holder, removeDuration, removeAnimator.itemDelayFactor)
+ duration = removeDuration
+ interpolator = this@KauAnimator.interpolator
+ removeAnimator.animation(holder)()
+ }
+ }
+
+ override fun removeAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ holder.itemView.apply { removeAnimator.animationCleanup(holder)() }
+ }
+
+ override fun getRemoveDelay(remove: Long, move: Long, change: Long): Long =
+ removeAnimator.getDelay(remove, move, change)
+
+ override fun addAnimationPrepare(holder: RecyclerView.ViewHolder) {
+ holder.itemView.apply { addAnimator.animationPrepare(holder)() }
+ }
+
+ override fun addAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return holder.itemView.animate().apply {
+ startDelay = startDelay(holder, addDuration, addAnimator.itemDelayFactor)
+ duration = addDuration
+ interpolator = this@KauAnimator.interpolator
+ addAnimator.animation(holder)()
+ }
+ }
+
+ override fun addAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ holder.itemView.apply { addAnimator.animationCleanup(holder)() }
+ }
+
+ override fun getAddDelay(remove: Long, move: Long, change: Long): Long = addAnimator.getDelay(remove, move, change)
+
+ override fun changeOldAnimation(holder: RecyclerView.ViewHolder, changeInfo: ChangeInfo): ViewPropertyAnimator {
+ return holder.itemView.animate().apply {
+ duration = changeDuration
+ interpolator = this@KauAnimator.interpolator
+ changeAnimator.changeOldAnimation(holder, changeInfo)()
+ }
+ }
+
+ override fun changeNewAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return holder.itemView.animate().apply {
+ duration = changeDuration
+ interpolator = this@KauAnimator.interpolator
+ changeAnimator.changeNewAnimation(holder)()
+ }
+ }
+
+ override fun changeAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ holder.itemView.apply { changeAnimator.changeAnimationCleanup(holder)() }
+ }
+}
diff --git a/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt b/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt
new file mode 100644
index 0000000..cca8a25
--- /dev/null
+++ b/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * 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 ca.allanwang.kau.animators
+
+import android.view.View
+import android.view.ViewPropertyAnimator
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * Created by Allan Wang on 2017-08-02.
+ */
+class NoAnimatorAdd(override var itemDelayFactor: Float = 0f) : KauAnimatorAdd {
+
+ override fun animationPrepare(holder: RecyclerView.ViewHolder): View.() -> Unit = {}
+
+ override fun animation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator.() -> Unit = {}
+
+ override fun animationCleanup(holder: RecyclerView.ViewHolder): View.() -> Unit = { }
+
+ override fun getDelay(remove: Long, move: Long, change: Long): Long = 0L
+}
+
+class NoAnimatorRemove(override var itemDelayFactor: Float = 0f) : KauAnimatorRemove {
+
+ override fun animation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator.() -> Unit = { }
+
+ override fun animationCleanup(holder: RecyclerView.ViewHolder): View.() -> Unit = {}
+
+ override fun getDelay(remove: Long, move: Long, change: Long): Long = 0L
+}
+
+class NoAnimatorChange : KauAnimatorChange {
+
+ override fun changeOldAnimation(
+ holder: RecyclerView.ViewHolder,
+ changeInfo: BaseItemAnimator.ChangeInfo
+ ): ViewPropertyAnimator.() -> Unit = { }
+
+ override fun changeNewAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator.() -> Unit = { }
+
+ override fun changeAnimationCleanup(holder: RecyclerView.ViewHolder): View.() -> Unit = { alpha = 1f }
+}
diff --git a/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/SlideAnimator.kt b/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/SlideAnimator.kt
new file mode 100644
index 0000000..55d5b2e
--- /dev/null
+++ b/fastadapter/src/main/kotlin/ca/allanwang/kau/animators/SlideAnimator.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * 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 ca.allanwang.kau.animators
+
+import android.view.View
+import android.view.ViewPropertyAnimator
+import androidx.recyclerview.widget.RecyclerView
+import ca.allanwang.kau.utils.KAU_BOTTOM
+import ca.allanwang.kau.utils.KAU_LEFT
+import ca.allanwang.kau.utils.KAU_RIGHT
+import ca.allanwang.kau.utils.KAU_TOP
+
+/**
+ * Created by Allan Wang on 2017-07-11.
+ */
+class SlideAnimatorAdd(val fromEdge: Int, val slideFactor: Float = 1f, override var itemDelayFactor: Float = 0.125f) :
+ KauAnimatorAdd {
+
+ override fun animationPrepare(holder: RecyclerView.ViewHolder): View.() -> Unit = {
+ when (fromEdge) {
+ KAU_TOP -> translationY = slideFactor * -height
+ KAU_LEFT -> translationX = slideFactor * -width
+ KAU_BOTTOM -> translationY = slideFactor * height
+ KAU_RIGHT -> translationX = slideFactor * width
+ else -> throw KauAnimatorException("Invalid edge flag used in Slide Animator; use one of KAU_*")
+ }
+ alpha = 0f
+ }
+
+ override fun animation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator.() -> Unit = {
+ translationY(0f)
+ translationX(0f)
+ alpha(1f)
+ }
+
+ override fun animationCleanup(holder: RecyclerView.ViewHolder): View.() -> Unit = {
+ translationY = 0f
+ translationX = 0f
+ alpha = 1f
+ }
+
+ override fun getDelay(remove: Long, move: Long, change: Long): Long = 0L
+}
+
+class SlideAnimatorRemove(
+ val fromEdge: Int,
+ val slideFactor: Float = 1f,
+ override var itemDelayFactor: Float = 0.125f
+) : KauAnimatorRemove {
+ override fun animation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator.() -> Unit = {
+ with(holder.itemView) {
+ when (fromEdge) {
+ KAU_TOP -> translationY(slideFactor * -height)
+ KAU_LEFT -> translationX(slideFactor * -width)
+ KAU_BOTTOM -> translationY(slideFactor * height)
+ KAU_RIGHT -> translationX(slideFactor * width)
+ else -> throw KauAnimatorException("Invalid edge flag used in Slide Animator; use one of KAU_*")
+ }
+ }
+ alpha(0f)
+ }
+
+ override fun animationCleanup(holder: RecyclerView.ViewHolder): View.() -> Unit = {
+ translationY = 0f
+ translationX = 0f
+ alpha = 1f
+ }
+
+ override fun getDelay(remove: Long, move: Long, change: Long): Long = 0L
+}
diff --git a/fastadapter/src/main/kotlin/ca/allanwang/kau/iitems/CardIItem.kt b/fastadapter/src/main/kotlin/ca/allanwang/kau/iitems/CardIItem.kt
new file mode 100644
index 0000000..6e33833
--- /dev/null
+++ b/fastadapter/src/main/kotlin/ca/allanwang/kau/iitems/CardIItem.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * 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 ca.allanwang.kau.iitems
+
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.cardview.widget.CardView
+import androidx.recyclerview.widget.RecyclerView
+import ca.allanwang.kau.adapter.R
+import ca.allanwang.kau.adapters.ThemableIItem
+import ca.allanwang.kau.adapters.ThemableIItemDelegate
+import ca.allanwang.kau.utils.INVALID_ID
+import ca.allanwang.kau.utils.drawable
+import ca.allanwang.kau.utils.gone
+import ca.allanwang.kau.utils.string
+import ca.allanwang.kau.utils.toDrawable
+import ca.allanwang.kau.utils.visible
+import com.mikepenz.fastadapter.FastAdapter
+import com.mikepenz.fastadapter.IItem
+import com.mikepenz.fastadapter.listeners.ClickEventHook
+import com.mikepenz.iconics.typeface.IIcon
+
+/**
+ * Created by Allan Wang on 2017-06-28.
+ *
+ * Simple generic card item with an icon, title, description and button
+ * The icon and button are hidden by default unless values are given
+ */
+class CardIItem(
+ val builder: Config.() -> Unit = {}
+) : KauIItem<CardIItem, CardIItem.ViewHolder>(
+ R.layout.kau_iitem_card, ::ViewHolder, R.id.kau_item_card
+), ThemableIItem by ThemableIItemDelegate() {
+
+ companion object {
+ fun bindClickEvents(fastAdapter: FastAdapter<IItem<*, *>>) {
+ fastAdapter.withEventHook(object : ClickEventHook<IItem<*, *>>() {
+ override fun onBindMany(viewHolder: RecyclerView.ViewHolder): List<View>? {
+ return if (viewHolder is ViewHolder) listOf(viewHolder.card, viewHolder.button) else null
+ }
+
+ override fun onClick(v: View, position: Int, adapter: FastAdapter<IItem<*, *>>, item: IItem<*, *>) {
+ if (item !is CardIItem) return
+ with(item.configs) {
+ when (v.id) {
+ R.id.kau_card_container -> cardClick?.invoke()
+ R.id.kau_card_button -> buttonClick?.invoke()
+ else -> {
+ }
+ }
+ }
+ }
+ })
+ }
+ }
+
+ val configs = Config().apply { builder() }
+
+ class Config {
+ var title: String? = null
+ var titleRes: Int = INVALID_ID
+ var desc: String? = null
+ var descRes: Int = INVALID_ID
+ var button: String? = null
+ var buttonRes: Int = INVALID_ID
+ var buttonClick: (() -> Unit)? = null
+ var cardClick: (() -> Unit)? = null
+ var image: Drawable? = null
+ var imageIIcon: IIcon? = null
+ var imageIIconColor: Int = Color.WHITE
+ var imageRes: Int = INVALID_ID
+ }
+
+ override fun bindView(holder: ViewHolder, payloads: MutableList<Any>) {
+ super.bindView(holder, payloads)
+ with(holder.itemView.context) context@{
+ with(configs) {
+ holder.title.text = string(titleRes, title)
+ val descText = string(descRes, desc)
+ if (descText != null) holder.description.visible().text = descText
+ val buttonText = string(buttonRes, button)
+ if (buttonText != null) {
+ holder.bottomRow.visible()
+ holder.button.text = buttonText
+ }
+ val icon = drawable(imageRes) {
+ imageIIcon?.toDrawable(this@context, sizeDp = 24, color = imageIIconColor)
+ ?: image
+ }
+ if (icon != null) holder.icon.visible().setImageDrawable(icon)
+ }
+ with(holder) {
+ bindTextColor(title)
+ bindTextColorSecondary(description)
+ bindAccentColor(button)
+ if (configs.imageIIcon != null) bindIconColor(icon)
+ bindBackgroundRipple(card)
+ }
+ }
+ }
+
+ override fun unbindView(holder: ViewHolder) {
+ super.unbindView(holder)
+ with(holder) {
+ icon.gone().setImageDrawable(null)
+ title.text = null
+ description.gone().text = null
+ bottomRow.gone()
+ button.setOnClickListener(null)
+ card.setOnClickListener(null)
+ }
+ }
+
+ class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
+ val card: CardView = v.findViewById(R.id.kau_card_container)
+ val icon: ImageView = v.findViewById(R.id.kau_card_image)
+ val title: TextView = v.findViewById(R.id.kau_card_title)
+ val description: TextView = v.findViewById(R.id.kau_card_description)
+ val bottomRow: LinearLayout = v.findViewById(R.id.kau_card_bottom_row)
+ val button: Button = v.findViewById(R.id.kau_card_button)
+ }
+}
diff --git a/fastadapter/src/main/kotlin/ca/allanwang/kau/iitems/HeaderIItem.kt b/fastadapter/src/main/kotlin/ca/allanwang/kau/iitems/HeaderIItem.kt
new file mode 100644
index 0000000..2c488b1
--- /dev/null
+++ b/fastadapter/src/main/kotlin/ca/allanwang/kau/iitems/HeaderIItem.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * 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 ca.allanwang.kau.iitems
+
+import android.view.View
+import android.widget.TextView
+import androidx.cardview.widget.CardView
+import androidx.recyclerview.widget.RecyclerView
+import ca.allanwang.kau.adapter.R
+import ca.allanwang.kau.adapters.ThemableIItem
+import ca.allanwang.kau.adapters.ThemableIItemDelegate
+import ca.allanwang.kau.utils.INVALID_ID
+import ca.allanwang.kau.utils.string
+
+/**
+ * Created by Allan Wang on 2017-06-28.
+ *
+ * Simple Header with lots of padding on the top
+ * Contains only one text view
+ */
+class HeaderIItem(
+ text: String? = null,
+ var textRes: Int = INVALID_ID
+) : KauIItem<HeaderIItem, HeaderIItem.ViewHolder>(
+ R.layout.kau_iitem_header, { ViewHolder(it) }, R.id.kau_item_header_big_margin_top
+), ThemableIItem by ThemableIItemDelegate() {
+
+ var text: String = text ?: "Header Placeholder"
+
+ override fun bindView(holder: ViewHolder, payloads: MutableList<Any>) {
+ super.bindView(holder, payloads)
+ holder.text.text = holder.itemView.context.string(textRes, text)
+ bindTextColor(holder.text)
+ bindBackgroundColor(holder.container)
+ }
+
+ override fun unbindView(holder: ViewHolder) {
+ super.unbindView(holder)
+ holder.text.text = null
+ }
+
+ class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
+ val text: TextView = v.findViewById(R.id.kau_header_text)
+ val container: CardView = v.findViewById(R.id.kau_header_container)
+ }
+}
diff --git a/fastadapter/src/main/kotlin/ca/allanwang/kau/iitems/KauIItem.kt b/fastadapter/src/main/kotlin/ca/allanwang/kau/iitems/KauIItem.kt
new file mode 100644
index 0000000..c66dc01
--- /dev/null
+++ b/fastadapter/src/main/kotlin/ca/allanwang/kau/iitems/KauIItem.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * 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 ca.allanwang.kau.iitems
+
+import android.annotation.SuppressLint
+import android.view.View
+import androidx.annotation.LayoutRes
+import androidx.recyclerview.widget.RecyclerView
+import com.mikepenz.fastadapter.IClickable
+import com.mikepenz.fastadapter.IItem
+import com.mikepenz.fastadapter.items.AbstractItem
+
+/**
+ * Created by Allan Wang on 2017-07-03.
+ *
+ * Kotlin implementation of the [AbstractItem] to make things shorter
+ * If only one iitem type extends the given [layoutRes], you may use it as the type and not worry about another id
+ */
+open class KauIItem<Item, VH : RecyclerView.ViewHolder>(
+ @param:LayoutRes private val layoutRes: Int,
+ private val viewHolder: (v: View) -> VH,
+ private val type: Int = layoutRes
+) : AbstractItem<Item, VH>() where Item : IItem<*, *>, Item : IClickable<*> {
+ @SuppressLint("ResourceType")
+ final override fun getType(): Int = type
+
+ final override fun getViewHolder(v: View): VH = viewHolder(v)
+ final override fun getLayoutRes(): Int = layoutRes
+}
diff --git a/fastadapter/src/main/res/layout/kau_iitem_card.xml b/fastadapter/src/main/res/layout/kau_iitem_card.xml
new file mode 100644
index 0000000..6bae0fe
--- /dev/null
+++ b/fastadapter/src/main/res/layout/kau_iitem_card.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Generic card with an imageview, title, description, and button
+ -->
+<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/kau_card_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground"
+ android:minHeight="?android:listPreferredItemHeight">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/kau_padding_normal">
+
+ <ImageView
+ android:id="@+id/kau_card_image"
+ android:layout_width="56dp"
+ android:layout_height="56dp"
+ android:layout_marginBottom="4dp"
+ android:layout_marginTop="4dp"
+ android:paddingEnd="32dp"
+ android:scaleType="fitCenter"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0" />
+
+ <TextView
+ android:id="@+id/kau_card_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/kau_card_image"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@+id/kau_card_description"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.AppCompat.Body1"
+ android:visibility="gone"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/kau_card_image"
+ app:layout_constraintTop_toBottomOf="@id/kau_card_title" />
+
+ <LinearLayout
+ android:id="@+id/kau_card_bottom_row"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/kau_card_image"
+ app:layout_constraintTop_toBottomOf="@id/kau_card_description">
+
+ <Button
+ android:id="@+id/kau_card_button"
+ style="?android:borderlessButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="?attr/colorAccent" />
+
+ </LinearLayout>
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.cardview.widget.CardView>
diff --git a/fastadapter/src/main/res/layout/kau_iitem_header.xml b/fastadapter/src/main/res/layout/kau_iitem_header.xml
new file mode 100644
index 0000000..b0b2ec8
--- /dev/null
+++ b/fastadapter/src/main/res/layout/kau_iitem_header.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/kau_header_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/kau_header_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/kau_spacing_xlarge"
+ android:padding="@dimen/kau_padding_normal"
+ android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
+
+</androidx.cardview.widget.CardView> \ No newline at end of file