aboutsummaryrefslogtreecommitdiff
path: root/library/src/main
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2017-06-27 23:25:51 -0700
committerAllan Wang <me@allanwang.ca>2017-06-27 23:25:51 -0700
commit53a4f63bc28b1f701d2bcf815ffa2fe2e931eba0 (patch)
treecbb132de624d3bd5efc1c64488a6640702a76264 /library/src/main
parenta6ee77a4f1d78252b15059d51f2533fa03483d8c (diff)
downloadkau-53a4f63bc28b1f701d2bcf815ffa2fe2e931eba0.tar.gz
kau-53a4f63bc28b1f701d2bcf815ffa2fe2e931eba0.tar.bz2
kau-53a4f63bc28b1f701d2bcf815ffa2fe2e931eba0.zip
Add animators
Diffstat (limited to 'library/src/main')
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt24
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/about/LibraryItem.kt8
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/about/MainItem.kt11
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/adapters/ChainedAdapters.kt15
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/adapters/SectionAdapter.kt13
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java764
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt62
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt41
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/animators/SlideUpAlphaAnimator.kt52
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt4
-rw-r--r--library/src/main/res/layout/kau_about_item_library.xml2
11 files changed, 970 insertions, 26 deletions
diff --git a/library/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt b/library/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt
index f469028..4690718 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt
@@ -1,20 +1,18 @@
package ca.allanwang.kau.about
import android.os.Bundle
-import android.os.PersistableBundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.Toolbar
import ca.allanwang.kau.R
import ca.allanwang.kau.adapters.ChainedAdapters
-import ca.allanwang.kau.logging.KL
+import ca.allanwang.kau.adapters.SectionAdapter
+import ca.allanwang.kau.animators.SlideUpAlphaAnimator
import ca.allanwang.kau.utils.bindView
import ca.allanwang.kau.utils.string
import ca.allanwang.kau.views.KauTextSlider
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.entity.Library
-import com.mikepenz.fastadapter.adapters.HeaderAdapter
-import com.mikepenz.fastadapter.adapters.ItemAdapter
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
@@ -38,17 +36,14 @@ open class AboutActivityBase(val rClass: Class<*>) : AppCompatActivity() {
val toolbar: Toolbar by bindView(R.id.kau_toolbar)
val toolbarText: KauTextSlider by bindView(R.id.kau_toolbar_text)
val recycler: RecyclerView by bindView(R.id.kau_recycler)
- val libSection: Pair<String, HeaderAdapter<LibraryItem>> by lazy { string(R.string.kau_dependencies_used) to HeaderAdapter<LibraryItem>() }
+ val libSection: Pair<String, SectionAdapter<LibraryItem>> by lazy { string(R.string.kau_dependencies_used) to SectionAdapter<LibraryItem>() }
val sectionsChain: ChainedAdapters<String> = ChainedAdapters()
fun addLibsAsync() {
doAsync {
val libs = Libs(this@AboutActivityBase, Libs.toStringArray(rClass.fields))
val items = getLibraries(libs)
- uiThread {
- KL.e("Coun ${items.count()}")
- libSection.second.add(items.map { LibraryItem(it) })
- }
+ uiThread { libSection.second.add(items.map { LibraryItem(it) }) }
}
}
@@ -69,12 +64,15 @@ open class AboutActivityBase(val rClass: Class<*>) : AppCompatActivity() {
if (dy > 0) toolbarText.setNextText(item)
else toolbarText.setPrevText()
}
+ recycler.itemAnimator = SlideUpAlphaAnimator()
toolbarText.setCurrentText(sectionsChain[0].first)
+ onPostCreate()
+ addLibsAsync()
}
- override fun onPostCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
- super.onPostCreate(savedInstanceState, persistentState)
- addLibsAsync()
+
+ open fun onPostCreate() {
+
}
/**
@@ -82,5 +80,5 @@ open class AboutActivityBase(val rClass: Class<*>) : AppCompatActivity() {
* The adapters should be listed in the order that they appear,
* and if the [libSection] shouldn't be at the end, it should be added in this list
*/
- open fun onCreateSections(): List<Pair<String, HeaderAdapter<*>>> = listOf(libSection)
+ open fun onCreateSections(): List<Pair<String, SectionAdapter<*>>> = listOf(libSection)
} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/about/LibraryItem.kt b/library/src/main/kotlin/ca/allanwang/kau/about/LibraryItem.kt
index 560eb82..0dd4973 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/about/LibraryItem.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/about/LibraryItem.kt
@@ -54,6 +54,14 @@ class LibraryItem(val lib: Library) : AbstractItem<LibraryItem, LibraryItem.View
}
}
+ override fun unbindView(holder: ViewHolder) {
+ super.unbindView(holder)
+ with (holder) {
+ name.text = null
+ creator.text = null
+ description.text = null
+ }
+ }
override fun getViewHolder(v: View): ViewHolder = ViewHolder(v)
diff --git a/library/src/main/kotlin/ca/allanwang/kau/about/MainItem.kt b/library/src/main/kotlin/ca/allanwang/kau/about/MainItem.kt
index decacfc..3edad32 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/about/MainItem.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/about/MainItem.kt
@@ -36,7 +36,7 @@ class MainItem(builder: Config.() -> Unit) : AbstractItem<MainItem, MainItem.Vie
override fun bindView(holder: ViewHolder, payloads: MutableList<Any>?) {
super.bindView(holder, payloads)
- with (holder) {
+ with(holder) {
title.text = configs.title
creator.text = configs.author
description.text = configs.description
@@ -44,6 +44,15 @@ class MainItem(builder: Config.() -> Unit) : AbstractItem<MainItem, MainItem.Vie
}
}
+ override fun unbindView(holder: ViewHolder) {
+ super.unbindView(holder)
+ with(holder) {
+ title.text = null
+ creator.text = null
+ description.text = null
+ }
+ }
+
class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
val card: CardView by bindView(R.id.container)
val title: TextView by bindView(R.id.title)
diff --git a/library/src/main/kotlin/ca/allanwang/kau/adapters/ChainedAdapters.kt b/library/src/main/kotlin/ca/allanwang/kau/adapters/ChainedAdapters.kt
index aaa0fb0..e1c5c18 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/adapters/ChainedAdapters.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/adapters/ChainedAdapters.kt
@@ -2,7 +2,6 @@ package ca.allanwang.kau.adapters
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
-import ca.allanwang.kau.logging.KL
import com.mikepenz.fastadapter.IItem
import com.mikepenz.fastadapter.adapters.HeaderAdapter
import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter
@@ -17,18 +16,19 @@ import java.util.*
* - 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, HeaderAdapter<*>>) {
- private val chain: MutableList<Pair<T, HeaderAdapter<*>>> = mutableListOf(*items)
+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, HeaderAdapter<*>>) = add(items.toList())
+ fun add(vararg items: Pair<T, SectionAdapter<*>>) = add(items.toList())
- fun add(items: Collection<Pair<T, HeaderAdapter<*>>>): ChainedAdapters<T> {
+ 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
}
@@ -51,7 +51,6 @@ class ChainedAdapters<T>(vararg items: Pair<T, HeaderAdapter<*>>) {
chain.map { it.second }.forEachReversedWithIndex { i, headerAdapter ->
if (i == chain.size - 1) headerAdapter.wrap(baseAdapter)
else headerAdapter.wrap(chain[i + 1].second)
- if (i < chain.size - 1) KL.e("${chain[i].first} ${chain[i + 1].first}")
}
recycler = recyclerView
indexStack.push(0)
@@ -68,10 +67,8 @@ class ChainedAdapters<T>(vararg items: Pair<T, HeaderAdapter<*>>) {
val nextAdapterIndex = (currentAdapterIndex until chain.size).asSequence()
.firstOrNull {
val adapter = chain[it].second
- KL.e("A $it ${adapter.adapterItemCount} ${adapter.getGlobalPosition(0)} $topPosition $currentAdapterIndex")
- adapter.adapterItemCount > 0 && adapter.getGlobalPosition(0) >= topPosition
+ adapter.adapterItemCount > 0 && adapter.getGlobalPosition(adapter.adapterItemCount - 1) >= topPosition
} ?: currentAdapterIndex
- KL.e("Next $nextAdapterIndex")
if (nextAdapterIndex == currentAdapterIndex) return
indexStack.push(nextAdapterIndex)
onAdapterSectionChanged(chain[indexStack.peek()].first, indexStack.peek(), dy)
diff --git a/library/src/main/kotlin/ca/allanwang/kau/adapters/SectionAdapter.kt b/library/src/main/kotlin/ca/allanwang/kau/adapters/SectionAdapter.kt
new file mode 100644
index 0000000..cf7205a
--- /dev/null
+++ b/library/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/library/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java b/library/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java
new file mode 100644
index 0000000..f25678f
--- /dev/null
+++ b/library/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<>();
+
+ 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/library/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt b/library/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt
new file mode 100644
index 0000000..abb22c8
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt
@@ -0,0 +1,62 @@
+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/library/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt b/library/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt
new file mode 100644
index 0000000..244287b
--- /dev/null
+++ b/library/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/library/src/main/kotlin/ca/allanwang/kau/animators/SlideUpAlphaAnimator.kt b/library/src/main/kotlin/ca/allanwang/kau/animators/SlideUpAlphaAnimator.kt
new file mode 100644
index 0000000..3b1b223
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/animators/SlideUpAlphaAnimator.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.
+ */
+class SlideUpAlphaAnimator : DefaultAnimator() {
+ override fun addAnimationPrepare(holder: RecyclerView.ViewHolder) {
+ with(holder.itemView) {
+ translationY = height.toFloat()
+ alpha = 0f
+ }
+ }
+
+ override fun addAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return holder.itemView.animate().apply {
+ translationY(0f)
+ alpha(0f)
+ duration = this@SlideUpAlphaAnimator.addDuration
+ interpolator = this@SlideUpAlphaAnimator.interpolator
+ }
+ }
+
+ public override fun addAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ with(holder.itemView) {
+ translationY = 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
+
+ override fun removeAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return holder.itemView.animate().apply {
+ duration = this@SlideUpAlphaAnimator.removeDuration
+ alpha(0f)
+ translationY(holder.itemView.height.toFloat())
+ interpolator = this@SlideUpAlphaAnimator.interpolator
+ }
+ }
+
+ override fun removeAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ with(holder.itemView) {
+ translationY = 0f
+ alpha = 1f
+ }
+ }
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt b/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt
index 29c2223..b2490ac 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt
@@ -11,13 +11,13 @@ import android.support.transition.AutoTransition
import android.support.v7.widget.AppCompatEditText
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
-import android.support.v7.widget.SimpleItemAnimator
import android.util.AttributeSet
import android.view.*
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.ProgressBar
import ca.allanwang.kau.R
+import ca.allanwang.kau.animators.NoAnimator
import ca.allanwang.kau.kotlin.nonReadable
import ca.allanwang.kau.searchview.SearchView.Configs
import ca.allanwang.kau.utils.*
@@ -247,7 +247,7 @@ class SearchView @JvmOverloads constructor(
}
})
adapter = this@SearchView.adapter
- (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false //clear the fade between item changes
+ itemAnimator = NoAnimator()
}
with(adapter) {
withSelectable(true)
diff --git a/library/src/main/res/layout/kau_about_item_library.xml b/library/src/main/res/layout/kau_about_item_library.xml
index 5730d67..0ae4d3c 100644
--- a/library/src/main/res/layout/kau_about_item_library.xml
+++ b/library/src/main/res/layout/kau_about_item_library.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
- android:id="@id/rippleForegroundListenerView"
+ android:id="@+id/rippleForegroundListenerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"