aboutsummaryrefslogtreecommitdiff
path: root/adapter
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2017-07-08 20:25:23 -0700
committerAllan Wang <me@allanwang.ca>2017-07-08 20:25:23 -0700
commit81996038462de1be86643e95d262933c4b96c551 (patch)
tree39423b28217251e7051f86e9e4f80c47bcba20b2 /adapter
parent880d433e475e5be4e5d91afac145b490f9a959b7 (diff)
downloadkau-81996038462de1be86643e95d262933c4b96c551.tar.gz
kau-81996038462de1be86643e95d262933c4b96c551.tar.bz2
kau-81996038462de1be86643e95d262933c4b96c551.zip
Move components to separate modules
Diffstat (limited to 'adapter')
-rw-r--r--adapter/.gitignore1
-rw-r--r--adapter/build.gradle61
-rw-r--r--adapter/progress-proguard.txt1
-rw-r--r--adapter/src/androidTest/java/ca/allanwang/kau/adapter/ExampleInstrumentedTest.java26
-rw-r--r--adapter/src/main/AndroidManifest.xml1
-rw-r--r--adapter/src/main/kotlin/ca/allanwang/kau/adapters/ChainedAdapters.kt85
-rw-r--r--adapter/src/main/kotlin/ca/allanwang/kau/adapters/FastItemThemedAdapter.kt189
-rw-r--r--adapter/src/main/kotlin/ca/allanwang/kau/adapters/SectionAdapter.kt13
-rw-r--r--adapter/src/main/kotlin/ca/allanwang/kau/animators/BaseDelayAnimator.kt45
-rw-r--r--adapter/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java764
-rw-r--r--adapter/src/main/kotlin/ca/allanwang/kau/animators/BaseSlideAlphaAnimator.kt52
-rw-r--r--adapter/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt63
-rw-r--r--adapter/src/main/kotlin/ca/allanwang/kau/animators/FadeScaleAnimator.kt51
-rw-r--r--adapter/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt41
-rw-r--r--adapter/src/main/kotlin/ca/allanwang/kau/animators/SlideUpExitRightAnimator.kt23
-rw-r--r--adapter/src/main/kotlin/ca/allanwang/kau/iitems/CardIItem.kt127
-rw-r--r--adapter/src/main/kotlin/ca/allanwang/kau/iitems/HeaderIItem.kt49
-rw-r--r--adapter/src/main/kotlin/ca/allanwang/kau/iitems/KauIItem.kt24
-rw-r--r--adapter/src/main/res/layout/kau_iitem_card.xml79
-rw-r--r--adapter/src/main/res/layout/kau_iitem_header.xml17
-rw-r--r--adapter/src/main/res/values/dimens.xml3
-rw-r--r--adapter/src/test/java/ca/allanwang/kau/adapter/ExampleUnitTest.java17
22 files changed, 1732 insertions, 0 deletions
diff --git a/adapter/.gitignore b/adapter/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/adapter/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/adapter/build.gradle b/adapter/build.gradle
new file mode 100644
index 0000000..82c89b0
--- /dev/null
+++ b/adapter/build.gradle
@@ -0,0 +1,61 @@
+plugins {
+ id 'com.gladed.androidgitversion' version '0.3.4'
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'com.github.dcendents.android-maven'
+
+group = project.APP_GROUP
+
+android {
+ compileSdkVersion Integer.parseInt(project.TARGET_SDK)
+ buildToolsVersion project.BUILD_TOOLS
+
+ androidGitVersion {
+ codeFormat = 'MMNNPPBB'
+ prefix 'v'
+ }
+
+ defaultConfig {
+ minSdkVersion Integer.parseInt(project.MIN_SDK)
+ targetSdkVersion Integer.parseInt(project.TARGET_SDK)
+ versionCode androidGitVersion.code()
+ versionName androidGitVersion.name()
+ consumerProguardFiles 'progress-proguard.txt'
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ lintOptions {
+ abortOnError false
+ checkReleaseBuilds false
+ }
+ resourcePrefix "kau_"
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ test.java.srcDirs += 'src/test/kotlin'
+ }
+}
+
+dependencies {
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
+ exclude group: 'com.android.support', module: 'support-annotations'
+ })
+ testCompile 'junit:junit:4.12'
+
+ compile project(':core')
+
+ compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
+
+ compile "com.mikepenz:fastadapter:${FAST_ADAPTER}@aar"
+ compile "com.mikepenz:fastadapter-commons:${FAST_ADAPTER_COMMONS}@aar"
+}
+
+apply from: '../artifacts.gradle'
diff --git a/adapter/progress-proguard.txt b/adapter/progress-proguard.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/adapter/progress-proguard.txt
@@ -0,0 +1 @@
+
diff --git a/adapter/src/androidTest/java/ca/allanwang/kau/adapter/ExampleInstrumentedTest.java b/adapter/src/androidTest/java/ca/allanwang/kau/adapter/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..3e64d38
--- /dev/null
+++ b/adapter/src/androidTest/java/ca/allanwang/kau/adapter/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package ca.allanwang.kau.adapter;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() throws Exception {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getTargetContext();
+
+ assertEquals("ca.allanwang.kau.adapter.test", appContext.getPackageName());
+ }
+}
diff --git a/adapter/src/main/AndroidManifest.xml b/adapter/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5d9b790
--- /dev/null
+++ b/adapter/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="ca.allanwang.kau.adapter" />
diff --git a/adapter/src/main/kotlin/ca/allanwang/kau/adapters/ChainedAdapters.kt b/adapter/src/main/kotlin/ca/allanwang/kau/adapters/ChainedAdapters.kt
new file mode 100644
index 0000000..e1c5c18
--- /dev/null
+++ b/adapter/src/main/kotlin/ca/allanwang/kau/adapters/ChainedAdapters.kt
@@ -0,0 +1,85 @@
+package ca.allanwang.kau.adapters
+
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.RecyclerView
+import com.mikepenz.fastadapter.IItem
+import com.mikepenz.fastadapter.adapters.HeaderAdapter
+import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter
+import org.jetbrains.anko.collections.forEachReversedWithIndex
+import java.util.*
+
+/**
+ * Created by Allan Wang on 2017-06-27.
+ *
+ * Once bounded to a [RecyclerView], this will
+ * - Chain together a list of [HeaderAdapter]s, backed by a generic [FastItemAdapter]
+ * - Add a [LinearLayoutManager] to the recycler
+ * - Add a listener for when a new adapter segment is being used
+ */
+class ChainedAdapters<T>(vararg items: Pair<T, SectionAdapter<*>>) {
+ private val chain: MutableList<Pair<T, SectionAdapter<*>>> = mutableListOf(*items)
+ val baseAdapter: FastItemAdapter<IItem<*, *>> = FastItemAdapter()
+ private val indexStack = Stack<Int>()
+ var recycler: RecyclerView? = null
+ val firstVisibleItemPosition: Int
+ get() = (recycler?.layoutManager as LinearLayoutManager?)?.findFirstVisibleItemPosition() ?: throw IllegalArgumentException("No recyclerview was bounded to the chain adapters")
+
+ fun add(vararg items: Pair<T, SectionAdapter<*>>) = add(items.toList())
+
+ fun add(items: Collection<Pair<T, SectionAdapter<*>>>): ChainedAdapters<T> {
+ if (recycler != null) throw IllegalAccessException("Chain adapter is already bounded to a recycler; cannot add directly.")
+ items.map { it.second }.forEachIndexed { index, sectionAdapter -> sectionAdapter.sectionOrder = chain.size + 1 + index }
+ chain.addAll(items)
+ return this
+ }
+
+ operator fun get(index: Int) = chain[index]
+
+ /**
+ * Attaches the chain to a recycler
+ * After this stage, any modifications to the adapters must be done through external references
+ * You may still get the generic header adapters through the get operator
+ * Binding the recycler also involves supplying a callback, which returns
+ * the item (T) associated with the adapter,
+ * the index (Int) of the current adapter
+ * and the dy (Int) as given by the scroll listener
+ */
+ fun bindRecyclerView(recyclerView: RecyclerView, onAdapterSectionChanged: (item: T, index: Int, dy: Int) -> Unit) {
+ if (recycler != null) throw IllegalStateException("Chain adapter is already bounded")
+ if (chain.isEmpty()) throw IllegalArgumentException("No adapters have been added to the adapters list")
+ //wrap adapters
+ chain.map { it.second }.forEachReversedWithIndex { i, headerAdapter ->
+ if (i == chain.size - 1) headerAdapter.wrap(baseAdapter)
+ else headerAdapter.wrap(chain[i + 1].second)
+ }
+ recycler = recyclerView
+ indexStack.push(0)
+ with(recyclerView) {
+ layoutManager = LinearLayoutManager(context)
+ adapter = chain.first().second
+ addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
+ super.onScrolled(rv, dx, dy)
+ val topPosition = firstVisibleItemPosition
+ val currentAdapterIndex = indexStack.peek()
+ if (dy > 0) {
+ //look ahead from current adapter
+ val nextAdapterIndex = (currentAdapterIndex until chain.size).asSequence()
+ .firstOrNull {
+ val adapter = chain[it].second
+ adapter.adapterItemCount > 0 && adapter.getGlobalPosition(adapter.adapterItemCount - 1) >= topPosition
+ } ?: currentAdapterIndex
+ if (nextAdapterIndex == currentAdapterIndex) return
+ indexStack.push(nextAdapterIndex)
+ onAdapterSectionChanged(chain[indexStack.peek()].first, indexStack.peek(), dy)
+ } else if (currentAdapterIndex == 0) {
+ return //All adapters may be empty; in this case, if we are already at the beginning, don't bother checking
+ } else if (chain[currentAdapterIndex].second.getGlobalPosition(0) > topPosition) {
+ indexStack.pop()
+ onAdapterSectionChanged(chain[indexStack.peek()].first, indexStack.peek(), dy)
+ }
+ }
+ })
+ }
+ }
+} \ No newline at end of file
diff --git a/adapter/src/main/kotlin/ca/allanwang/kau/adapters/FastItemThemedAdapter.kt b/adapter/src/main/kotlin/ca/allanwang/kau/adapters/FastItemThemedAdapter.kt
new file mode 100644
index 0000000..0de1dca
--- /dev/null
+++ b/adapter/src/main/kotlin/ca/allanwang/kau/adapters/FastItemThemedAdapter.kt
@@ -0,0 +1,189 @@
+package ca.allanwang.kau.adapters
+
+import android.content.res.ColorStateList
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import ca.allanwang.kau.utils.adjustAlpha
+import ca.allanwang.kau.ui.createSimpleRippleDrawable
+import com.mikepenz.fastadapter.IExpandable
+import com.mikepenz.fastadapter.IItem
+import com.mikepenz.fastadapter.ISubItem
+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: MutableList<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: MutableList<Item>): FastItemAdapter<Item> {
+ injectTheme(items)
+ injectTheme(items)
+ return super.add(items)
+ }
+
+ override fun set(items: MutableList<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: MutableList<Item>?, retainFilter: Boolean): FastItemAdapter<Item> {
+ injectTheme(items)
+ return super.setNewList(items, retainFilter)
+ }
+
+ override fun setNewList(items: MutableList<Item>?): FastItemAdapter<Item> {
+ injectTheme(items)
+ return super.setNewList(items)
+ }
+
+ override fun <T, S> setSubItems(collapsible: T, subItems: MutableList<S>?): T where S : IItem<*, *>?, T : IItem<*, *>?, T : IExpandable<T, S>?, S : ISubItem<Item, T>? {
+ injectTheme(subItems)
+ return super.setSubItems(collapsible, subItems)
+ }
+
+ internal fun injectTheme(items: Collection<IItem<*, *>?>?) {
+ items?.forEach { injectTheme(it) }
+ }
+
+ internal 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) }
+ }
+
+ override fun bindBackgroundRipple(vararg views: View?) {
+ val foreground = accentColor ?: textColor ?: return
+ val background = backgroundColor ?: return
+ val ripple = createSimpleRippleDrawable(foreground, background)
+ views.forEach { it?.background = ripple }
+ }
+
+ override fun bindIconColor(vararg views: ImageView?) {
+ val color = accentColor ?: textColor ?: return
+ views.forEach { it?.drawable?.setTintList(ColorStateList.valueOf(color)) }
+ }
+} \ No newline at end of file
diff --git a/adapter/src/main/kotlin/ca/allanwang/kau/adapters/SectionAdapter.kt b/adapter/src/main/kotlin/ca/allanwang/kau/adapters/SectionAdapter.kt
new file mode 100644
index 0000000..cf7205a
--- /dev/null
+++ b/adapter/src/main/kotlin/ca/allanwang/kau/adapters/SectionAdapter.kt
@@ -0,0 +1,13 @@
+package ca.allanwang.kau.adapters
+
+import com.mikepenz.fastadapter.IItem
+import com.mikepenz.fastadapter.adapters.HeaderAdapter
+
+/**
+ * Created by Allan Wang on 2017-06-27.
+ *
+ * Extension of [HeaderAdapter] where we can define the order
+ */
+class SectionAdapter<Item : IItem<*, *>>(var sectionOrder: Int = 100) : HeaderAdapter<Item>() {
+ override fun getOrder(): Int = sectionOrder
+} \ No newline at end of file
diff --git a/adapter/src/main/kotlin/ca/allanwang/kau/animators/BaseDelayAnimator.kt b/adapter/src/main/kotlin/ca/allanwang/kau/animators/BaseDelayAnimator.kt
new file mode 100644
index 0000000..c649376
--- /dev/null
+++ b/adapter/src/main/kotlin/ca/allanwang/kau/animators/BaseDelayAnimator.kt
@@ -0,0 +1,45 @@
+package ca.allanwang.kau.animators
+
+import android.support.v7.widget.RecyclerView
+import android.view.ViewPropertyAnimator
+
+/**
+ * Created by Allan Wang on 2017-06-27.
+ *
+ * Base for delayed animators
+ * item delay factor by default can be 0.125f
+ */
+abstract class BaseDelayAnimator(val itemDelayFactor: Float) : DefaultAnimator() {
+
+ override abstract fun addAnimationPrepare(holder: RecyclerView.ViewHolder)
+
+ override fun addAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return holder.itemView.animate().apply {
+ startDelay = Math.max(0L, (holder.adapterPosition * addDuration * itemDelayFactor).toLong())
+ duration = this@BaseDelayAnimator.addDuration
+ interpolator = this@BaseDelayAnimator.interpolator
+ }
+ }
+
+
+ override abstract fun addAnimationCleanup(holder: RecyclerView.ViewHolder)
+
+ override fun getAddDelay(remove: Long, move: Long, change: Long): Long = 0
+
+ override fun getRemoveDelay(remove: Long, move: Long, change: Long): Long = 0
+
+ /**
+ * Partial removal animation
+ * As of now, all it does it change the alpha
+ * To have it slide, add onto it in a sub class
+ */
+ override fun removeAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return holder.itemView.animate().apply {
+ duration = this@BaseDelayAnimator.removeDuration
+ startDelay = Math.max(0L, (holder.adapterPosition * removeDuration * itemDelayFactor).toLong())
+ interpolator = this@BaseDelayAnimator.interpolator
+ }
+ }
+
+ override abstract fun removeAnimationCleanup(holder: RecyclerView.ViewHolder)
+} \ No newline at end of file
diff --git a/adapter/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java b/adapter/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java
new file mode 100644
index 0000000..69c2cf3
--- /dev/null
+++ b/adapter/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java
@@ -0,0 +1,764 @@
+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 android.support.annotation.NonNull;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.support.v7.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 android.support.v7.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 android.support.v7.widget.RecyclerView#setItemAnimator(android.support.v7.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 tro 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/adapter/src/main/kotlin/ca/allanwang/kau/animators/BaseSlideAlphaAnimator.kt b/adapter/src/main/kotlin/ca/allanwang/kau/animators/BaseSlideAlphaAnimator.kt
new file mode 100644
index 0000000..a963358
--- /dev/null
+++ b/adapter/src/main/kotlin/ca/allanwang/kau/animators/BaseSlideAlphaAnimator.kt
@@ -0,0 +1,52 @@
+package ca.allanwang.kau.animators
+
+import android.support.v7.widget.RecyclerView
+import android.view.ViewPropertyAnimator
+
+/**
+ * Created by Allan Wang on 2017-06-27.
+ *
+ * Base for sliding animators
+ * item delay factor by default can be 0.125f
+ */
+abstract class BaseSlideAlphaAnimator(itemDelayFactor: Float) : BaseDelayAnimator(itemDelayFactor) {
+
+ override fun addAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return super.addAnimation(holder).apply {
+ translationY(0f)
+ translationX(0f)
+ alpha(1f)
+ }
+ }
+
+ final override fun addAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ with(holder.itemView) {
+ translationY = 0f
+ translationX = 0f
+ alpha = 1f
+ }
+ }
+
+ override fun getAddDelay(remove: Long, move: Long, change: Long): Long = 0
+
+ override fun getRemoveDelay(remove: Long, move: Long, change: Long): Long = 0
+
+ /**
+ * Partial removal animation
+ * As of now, all it does it change the alpha
+ * To have it slide, add onto it in a sub class
+ */
+ override fun removeAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return super.addAnimation(holder).apply {
+ alpha(0f)
+ }
+ }
+
+ override final fun removeAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ with(holder.itemView) {
+ translationY = 0f
+ translationX = 0f
+ alpha = 1f
+ }
+ }
+} \ No newline at end of file
diff --git a/adapter/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt b/adapter/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt
new file mode 100644
index 0000000..9aeafde
--- /dev/null
+++ b/adapter/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt
@@ -0,0 +1,63 @@
+package ca.allanwang.kau.animators
+
+import android.support.v7.widget.RecyclerView
+import android.view.ViewPropertyAnimator
+
+/**
+ * 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
+ }
+
+} \ No newline at end of file
diff --git a/adapter/src/main/kotlin/ca/allanwang/kau/animators/FadeScaleAnimator.kt b/adapter/src/main/kotlin/ca/allanwang/kau/animators/FadeScaleAnimator.kt
new file mode 100644
index 0000000..e968cda
--- /dev/null
+++ b/adapter/src/main/kotlin/ca/allanwang/kau/animators/FadeScaleAnimator.kt
@@ -0,0 +1,51 @@
+package ca.allanwang.kau.animators
+
+import android.support.v7.widget.RecyclerView
+import android.view.ViewPropertyAnimator
+
+/**
+ * Created by Allan Wang on 2017-06-29.
+ */
+open class FadeScaleAnimator(val scaleFactor: Float = 0.7f, itemDelayFactor: Float = 0.125f) : BaseDelayAnimator(itemDelayFactor) {
+
+ override fun addAnimationPrepare(holder: RecyclerView.ViewHolder) {
+ with(holder.itemView) {
+ scaleX = scaleFactor
+ scaleY = scaleFactor
+ alpha = 0f
+ }
+ }
+
+ override final fun addAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return super.addAnimation(holder).apply {
+ scaleX(1f)
+ scaleY(1f)
+ alpha(1f)
+ }
+ }
+
+
+ final override fun addAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ with(holder.itemView) {
+ scaleX = 1f
+ scaleY = 1f
+ alpha = 1f
+ }
+ }
+
+ override fun removeAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return super.removeAnimation(holder).apply {
+ scaleX(scaleFactor)
+ scaleY(scaleFactor)
+ alpha(0f)
+ }
+ }
+
+ override final fun removeAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ with(holder.itemView) {
+ translationY = 0f
+ translationX = 0f
+ alpha = 1f
+ }
+ }
+} \ No newline at end of file
diff --git a/adapter/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt b/adapter/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt
new file mode 100644
index 0000000..244287b
--- /dev/null
+++ b/adapter/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt
@@ -0,0 +1,41 @@
+package ca.allanwang.kau.animators
+
+import android.support.v7.widget.RecyclerView
+import android.view.ViewPropertyAnimator
+
+/**
+ * Created by Allan Wang on 2017-06-27.
+ *
+ * Truly have no animation
+ */
+class NoAnimator : DefaultAnimator() {
+ override fun addAnimationPrepare(holder: RecyclerView.ViewHolder) {}
+
+ override fun addAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator = holder.itemView.animate().apply { duration = 0 }
+
+ override fun addAnimationCleanup(holder: RecyclerView.ViewHolder) {}
+
+ override fun changeOldAnimation(holder: RecyclerView.ViewHolder, changeInfo: ChangeInfo): ViewPropertyAnimator = holder.itemView.animate().apply { duration = 0 }
+
+ override fun changeNewAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator = holder.itemView.animate().apply { duration = 0 }
+
+ override fun changeAnimationCleanup(holder: RecyclerView.ViewHolder) {}
+
+ override fun changeAnimation(oldHolder: RecyclerView.ViewHolder, newHolder: RecyclerView.ViewHolder?, fromX: Int, fromY: Int, toX: Int, toY: Int) {}
+
+ override fun getAddDelay(remove: Long, move: Long, change: Long): Long = 0
+
+ override fun getAddDuration(): Long = 0
+
+ override fun getMoveDuration(): Long = 0
+
+ override fun getRemoveDuration(): Long = 0
+
+ override fun getChangeDuration(): Long = 0
+
+ override fun getRemoveDelay(remove: Long, move: Long, change: Long): Long = 0
+
+ override fun removeAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator = holder.itemView.animate().apply { duration = 0 }
+
+ override fun removeAnimationCleanup(holder: RecyclerView.ViewHolder) {}
+} \ No newline at end of file
diff --git a/adapter/src/main/kotlin/ca/allanwang/kau/animators/SlideUpExitRightAnimator.kt b/adapter/src/main/kotlin/ca/allanwang/kau/animators/SlideUpExitRightAnimator.kt
new file mode 100644
index 0000000..8670493
--- /dev/null
+++ b/adapter/src/main/kotlin/ca/allanwang/kau/animators/SlideUpExitRightAnimator.kt
@@ -0,0 +1,23 @@
+package ca.allanwang.kau.animators
+
+import android.support.v7.widget.RecyclerView
+import android.view.ViewPropertyAnimator
+
+/**
+ * Created by Allan Wang on 2017-06-27.
+ */
+class SlideUpExitRightAnimator(itemDelayFactor: Float = 0.125f) : BaseSlideAlphaAnimator(itemDelayFactor) {
+
+ override fun addAnimationPrepare(holder: RecyclerView.ViewHolder) {
+ with(holder.itemView) {
+ translationY = height.toFloat()
+ alpha = 0f
+ }
+ }
+
+ override fun removeAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return super.removeAnimation(holder).apply {
+ translationX(holder.itemView.width.toFloat())
+ }
+ }
+} \ No newline at end of file
diff --git a/adapter/src/main/kotlin/ca/allanwang/kau/iitems/CardIItem.kt b/adapter/src/main/kotlin/ca/allanwang/kau/iitems/CardIItem.kt
new file mode 100644
index 0000000..3380ade
--- /dev/null
+++ b/adapter/src/main/kotlin/ca/allanwang/kau/iitems/CardIItem.kt
@@ -0,0 +1,127 @@
+package ca.allanwang.kau.iitems
+
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.support.v7.widget.CardView
+import android.support.v7.widget.RecyclerView
+import android.view.View
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import ca.allanwang.kau.R
+import ca.allanwang.kau.adapters.ThemableIItem
+import ca.allanwang.kau.adapters.ThemableIItemDelegate
+import ca.allanwang.kau.utils.*
+import com.mikepenz.fastadapter.FastAdapter
+import com.mikepenz.fastadapter.IItem
+import com.mikepenz.fastadapter.items.AbstractItem
+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 = {}
+) : AbstractItem<CardIItem, CardIItem.ViewHolder>(), ThemableIItem by ThemableIItemDelegate() {
+
+ companion object {
+ @JvmStatic 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?.onClick(v)
+ R.id.kau_card_button -> buttonClick?.onClick(v)
+ else -> {
+ }
+ }
+ }
+ }
+ })
+ }
+ }
+
+ val configs = Config().apply { builder() }
+
+ class Config {
+ var title: String? = null
+ var titleRes: Int = -1
+ var desc: String? = null
+ var descRes: Int = -1
+ var button: String? = null
+ var buttonRes: Int = -1
+ var buttonClick: View.OnClickListener? = null
+ var cardClick: View.OnClickListener? = null
+ var image: Drawable? = null
+ var imageIIcon: IIcon? = null
+ var imageIIconColor: Int = Color.WHITE
+ var imageRes: Int = -1
+ }
+
+
+ override fun getType(): Int = R.id.kau_item_card
+
+ override fun getLayoutRes(): Int = R.layout.kau_iitem_card
+
+ 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)
+ holder.description.text = string(descRes, desc)
+ val buttonText = string(buttonRes, button)
+ if (buttonText != null) {
+ holder.bottomRow.visible()
+ holder.button.text = buttonText
+ holder.button.setOnClickListener(buttonClick)
+ }
+ holder.icon.setImageDrawable(
+ if (imageRes > 0) drawable(imageRes)
+ else if (imageIIcon != null) imageIIcon!!.toDrawable(this@context, sizeDp = 40, color = imageIIconColor)
+ else image
+ )
+ holder.card.setOnClickListener(cardClick)
+ }
+ 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.text = null
+ bottomRow.gone()
+ button.setOnClickListener(null)
+ card.setOnClickListener(null)
+ }
+ }
+
+ override fun getViewHolder(v: View): ViewHolder = ViewHolder(v)
+
+ class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
+ val card: CardView by bindView(R.id.kau_card_container)
+ val icon: ImageView by bindView(R.id.kau_card_image)
+ val title: TextView by bindView(R.id.kau_card_title)
+ val description: TextView by bindView(R.id.kau_card_description)
+ val bottomRow: LinearLayout by bindView(R.id.kau_card_bottom_row)
+ val button: Button by bindView(R.id.kau_card_button)
+ }
+
+} \ No newline at end of file
diff --git a/adapter/src/main/kotlin/ca/allanwang/kau/iitems/HeaderIItem.kt b/adapter/src/main/kotlin/ca/allanwang/kau/iitems/HeaderIItem.kt
new file mode 100644
index 0000000..e994781
--- /dev/null
+++ b/adapter/src/main/kotlin/ca/allanwang/kau/iitems/HeaderIItem.kt
@@ -0,0 +1,49 @@
+package ca.allanwang.kau.iitems
+
+import android.support.v7.widget.CardView
+import android.support.v7.widget.RecyclerView
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import ca.allanwang.kau.R
+import ca.allanwang.kau.adapters.ThemableIItem
+import ca.allanwang.kau.adapters.ThemableIItemDelegate
+import ca.allanwang.kau.utils.bindView
+import ca.allanwang.kau.utils.string
+import com.mikepenz.fastadapter.items.AbstractItem
+
+/**
+ * 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 = -1
+) : AbstractItem<HeaderIItem, HeaderIItem.ViewHolder>(), ThemableIItem by ThemableIItemDelegate() {
+
+ var text: String = text ?: "Header Placeholder"
+
+ override fun getType(): Int = R.id.kau_item_header_big_margin_top
+
+ override fun getLayoutRes(): Int = R.layout.kau_iitem_header
+
+ 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
+ }
+
+ override fun getViewHolder(v: View): ViewHolder = ViewHolder(v)
+
+ class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
+ val text: TextView by bindView(R.id.kau_header_text)
+ val container: CardView by bindView(R.id.kau_header_container)
+ }
+
+} \ No newline at end of file
diff --git a/adapter/src/main/kotlin/ca/allanwang/kau/iitems/KauIItem.kt b/adapter/src/main/kotlin/ca/allanwang/kau/iitems/KauIItem.kt
new file mode 100644
index 0000000..d8567c4
--- /dev/null
+++ b/adapter/src/main/kotlin/ca/allanwang/kau/iitems/KauIItem.kt
@@ -0,0 +1,24 @@
+package ca.allanwang.kau.iitems
+
+import android.support.annotation.LayoutRes
+import android.support.v7.widget.RecyclerView
+import android.view.View
+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<*> {
+ override final fun getType(): Int = type
+ override final fun getViewHolder(v: View): VH = viewHolder(v)
+ override final fun getLayoutRes(): Int = layoutRes
+} \ No newline at end of file
diff --git a/adapter/src/main/res/layout/kau_iitem_card.xml b/adapter/src/main/res/layout/kau_iitem_card.xml
new file mode 100644
index 0000000..621da2e
--- /dev/null
+++ b/adapter/src/main/res/layout/kau_iitem_card.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Generic card with an imageview, title, description, and button
+ -->
+<android.support.v7.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:layout_marginBottom="@dimen/kau_padding_normal"
+ android:background="?android:selectableItemBackground"
+ android:minHeight="?android:listPreferredItemHeight">
+
+ <android.support.constraint.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/kau_padding_normal"
+ android:paddingEnd="@dimen/kau_padding_normal"
+ android:paddingTop="@dimen/kau_padding_normal">
+
+ <ImageView
+ android:id="@+id/kau_card_image"
+ android:layout_width="@dimen/kau_avatar_bounds"
+ android:layout_height="@dimen/kau_avatar_bounds"
+ android:layout_marginEnd="@dimen/kau_avatar_margin"
+ android:layout_marginStart="@dimen/kau_avatar_margin"
+ android:padding="@dimen/kau_avatar_padding"
+ android:scaleType="fitCenter"
+ android:visibility="gone"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+
+ <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"
+ app:layout_goneMarginStart="@dimen/kau_padding_normal" />
+
+ <TextView
+ android:id="@+id/kau_card_description"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.AppCompat.Body1"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/kau_card_image"
+ app:layout_constraintTop_toBottomOf="@id/kau_card_title"
+ app:layout_goneMarginStart="@dimen/kau_padding_normal" />
+
+ <LinearLayout
+ android:id="@+id/kau_card_bottom_row"
+ android:layout_width="match_parent"
+ 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"
+ app:layout_goneMarginStart="@dimen/kau_padding_normal">
+
+ <Button
+ android:id="@+id/kau_card_button"
+ style="?android:borderlessButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/kau_spacing_normal"
+ android:textColor="?attr/colorAccent" />
+
+ </LinearLayout>
+
+ </android.support.constraint.ConstraintLayout>
+
+</android.support.v7.widget.CardView>
diff --git a/adapter/src/main/res/layout/kau_iitem_header.xml b/adapter/src/main/res/layout/kau_iitem_header.xml
new file mode 100644
index 0000000..fa5a595
--- /dev/null
+++ b/adapter/src/main/res/layout/kau_iitem_header.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v7.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:layout_marginBottom="@dimen/kau_padding_normal"
+ 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" />
+
+</android.support.v7.widget.CardView> \ No newline at end of file
diff --git a/adapter/src/main/res/values/dimens.xml b/adapter/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..193940e
--- /dev/null
+++ b/adapter/src/main/res/values/dimens.xml
@@ -0,0 +1,3 @@
+<resources>
+ <dimen name="kau_color_circle_size">56dp</dimen>
+</resources>
diff --git a/adapter/src/test/java/ca/allanwang/kau/adapter/ExampleUnitTest.java b/adapter/src/test/java/ca/allanwang/kau/adapter/ExampleUnitTest.java
new file mode 100644
index 0000000..5684bab
--- /dev/null
+++ b/adapter/src/test/java/ca/allanwang/kau/adapter/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package ca.allanwang.kau.adapter;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+} \ No newline at end of file