diff options
author | Allan Wang <me@allanwang.ca> | 2017-07-08 20:25:23 -0700 |
---|---|---|
committer | Allan Wang <me@allanwang.ca> | 2017-07-08 20:25:23 -0700 |
commit | 81996038462de1be86643e95d262933c4b96c551 (patch) | |
tree | 39423b28217251e7051f86e9e4f80c47bcba20b2 /adapter | |
parent | 880d433e475e5be4e5d91afac145b490f9a959b7 (diff) | |
download | kau-81996038462de1be86643e95d262933c4b96c551.tar.gz kau-81996038462de1be86643e95d262933c4b96c551.tar.bz2 kau-81996038462de1be86643e95d262933c4b96c551.zip |
Move components to separate modules
Diffstat (limited to 'adapter')
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 |