summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoremancebo <emancebo@cyngn.com>2014-10-28 14:23:09 -0700
committeremancebo <emancebo@cyngn.com>2014-10-28 14:44:43 -0700
commit163ca059f6df860c87e490e27b1bceeb3d614f33 (patch)
treec370cc5c2e60483eda0f06150df1020744197e9f
parentf185b073929b090a8bd557341fee177b88ab982e (diff)
downloadandroid_external_cyanogen_UICommon-163ca059f6df860c87e490e27b1bceeb3d614f33.tar.gz
android_external_cyanogen_UICommon-163ca059f6df860c87e490e27b1bceeb3d614f33.tar.bz2
android_external_cyanogen_UICommon-163ca059f6df860c87e490e27b1bceeb3d614f33.zip
UICommon: Add ExpandingCard view
Change-Id: Iabe57f5636c4f3bd7ed543b0d29eed72240ff00f
-rw-r--r--res/drawable-hdpi/bg_contact_card.9.pngbin0 -> 377 bytes
-rw-r--r--res/drawable-mdpi/bg_contact_card.9.pngbin0 -> 255 bytes
-rw-r--r--res/drawable-xhdpi/bg_contact_card.9.pngbin0 -> 448 bytes
-rw-r--r--res/drawable-xxhdpi/bg_contact_card.9.pngbin0 -> 601 bytes
-rw-r--r--res/layout/expanding_card.xml35
-rw-r--r--res/values/colors.xml4
-rw-r--r--res/values/dimens.xml4
-rw-r--r--src/com/cyngn/uicommon/view/ExpandingCard.java375
8 files changed, 418 insertions, 0 deletions
diff --git a/res/drawable-hdpi/bg_contact_card.9.png b/res/drawable-hdpi/bg_contact_card.9.png
new file mode 100644
index 0000000..dc90e23
--- /dev/null
+++ b/res/drawable-hdpi/bg_contact_card.9.png
Binary files differ
diff --git a/res/drawable-mdpi/bg_contact_card.9.png b/res/drawable-mdpi/bg_contact_card.9.png
new file mode 100644
index 0000000..f316b61
--- /dev/null
+++ b/res/drawable-mdpi/bg_contact_card.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/bg_contact_card.9.png b/res/drawable-xhdpi/bg_contact_card.9.png
new file mode 100644
index 0000000..aab782d
--- /dev/null
+++ b/res/drawable-xhdpi/bg_contact_card.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi/bg_contact_card.9.png b/res/drawable-xxhdpi/bg_contact_card.9.png
new file mode 100644
index 0000000..2fd9cbd
--- /dev/null
+++ b/res/drawable-xxhdpi/bg_contact_card.9.png
Binary files differ
diff --git a/res/layout/expanding_card.xml b/res/layout/expanding_card.xml
new file mode 100644
index 0000000..6e33885
--- /dev/null
+++ b/res/layout/expanding_card.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.cyngn.uicommon.view.ExpandingCard
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/expandingCard"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/expanding_card_color">
+
+ <FrameLayout
+ android:id="@+id/shadowView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="2dp"
+ android:layout_marginBottom="4dp"
+ android:background="@drawable/bg_contact_card"/>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="2dp"
+ android:layout_marginBottom="4dp">
+ <FrameLayout
+ android:id="@+id/auxiliaryView"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/expanding_card_aux_height"
+ android:background="@color/expanding_card_color"
+ android:layout_gravity="bottom"/>
+
+ <FrameLayout
+ android:id="@+id/mainView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/expanding_card_color"/>
+ </FrameLayout>
+</com.cyngn.uicommon.view.ExpandingCard> \ No newline at end of file
diff --git a/res/values/colors.xml b/res/values/colors.xml
new file mode 100644
index 0000000..00cb435
--- /dev/null
+++ b/res/values/colors.xml
@@ -0,0 +1,4 @@
+<resources>
+ <color name="expanding_card_color">#f5f5f5</color>
+ <color name="expanding_card_selected_color">#ffffff</color>
+</resources> \ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
new file mode 100644
index 0000000..9d0c4ee
--- /dev/null
+++ b/res/values/dimens.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="expanding_card_aux_height">50dp</dimen>
+</resources>
diff --git a/src/com/cyngn/uicommon/view/ExpandingCard.java b/src/com/cyngn/uicommon/view/ExpandingCard.java
new file mode 100644
index 0000000..c80cfa9
--- /dev/null
+++ b/src/com/cyngn/uicommon/view/ExpandingCard.java
@@ -0,0 +1,375 @@
+package com.cyngn.uicommon.view;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ArgbEvaluator;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ListView;
+import com.cyngn.uicommon.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An informational view that has an auxiliary view that slides out from the bottom.
+ * This auxiliary view is typically used to show actions that can be taken on the
+ * main view.
+ */
+public class ExpandingCard extends FrameLayout {
+
+ private static final int EXPAND_DURATION = 150;
+
+ public static enum AnimationType {
+ // the bottom of the aux view is anchored and the content view slides
+ // up to reveal it
+ ANCHOR_BOTTOM,
+
+ // the top of the main view is anchored and the aux view slides out
+ // from the bottom
+ ANCHOR_TOP,
+
+ // no animation, anchor to the top
+ NONE,
+ }
+
+ private View mExpandingCard;
+ private View mMainView;
+ private View mAuxView;
+ private View mShadowView;
+
+ private int mAuxTop = -1;
+ private int mAuxHeight;
+ private int mMainBottom = -1;
+ private int mColor;
+ private int mColorSelected;
+ private int mShadowMarginTop;
+ private int mShadownMarginBottom;
+
+ public ExpandingCard(Context context) {
+ super(context);
+ }
+
+ public ExpandingCard(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ExpandingCard(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mExpandingCard = findViewById(R.id.expandingCard);
+ mMainView = findViewById(R.id.mainView);
+ mAuxView = findViewById(R.id.auxiliaryView);
+ mShadowView = findViewById(R.id.shadowView);
+
+ Resources res = getResources();
+ mAuxHeight = res.getDimensionPixelSize(R.dimen.expanding_card_aux_height);
+ mColor = res.getColor(R.color.expanding_card_color);
+ mColorSelected = res.getColor(R.color.expanding_card_selected_color);
+
+ MarginLayoutParams lp = (MarginLayoutParams)mShadowView.getLayoutParams();
+ mShadowMarginTop = lp.topMargin;
+ mShadownMarginBottom = lp.bottomMargin;
+ }
+
+ /**
+ * Expand the card using the given type of animation
+ *
+ * @param type
+ */
+ public void expand(AnimationType type) {
+ mAuxView.setVisibility(View.VISIBLE);
+
+ int mid = getAuxTop();
+ int bottom = getMainBottom();
+
+ ValueAnimator anim = null;
+ switch(type) {
+ case ANCHOR_BOTTOM:
+ anim = ValueAnimator.ofInt(0, bottom - mid);
+ anim.addUpdateListener(
+ new MarginTwiddler(mMainView, new BottomMarginSetter()));
+ break;
+ case ANCHOR_TOP:
+ anim = ValueAnimator.ofInt(mid, bottom);
+ anim.addUpdateListener(
+ new MarginTwiddler(mAuxView, new TopMarginSetter()));
+ break;
+ case NONE:
+ new TopMarginSetter().setMargin(mAuxView, bottom);
+ mMainView.setBackgroundColor(mColorSelected);
+ mAuxView.setBackgroundColor(mColorSelected);
+ break;
+ }
+
+ if (anim != null) {
+ List<Animator> animations = getColorAnimations(mColor, mColorSelected);
+ animations.add(anim);
+ animations.addAll(getShadowAnimations(true));
+
+ AnimatorSet set = new AnimatorSet();
+ set.playTogether(animations);
+ set.setDuration(EXPAND_DURATION);
+ set.start();
+ }
+ }
+
+ /**
+ * Collapse the card using the same anchor that was used to expand it.
+ */
+ public void collapse() {
+ MarginLayoutParams mlp = (MarginLayoutParams)mMainView.getLayoutParams();
+ MarginLayoutParams alp = (MarginLayoutParams)mAuxView.getLayoutParams();
+
+ // whichever margin is out of alignment due to an expand, animate that margin back
+ // to its original position
+ ValueAnimator anim = null;
+ if (mlp.bottomMargin > 0) {
+ anim = ValueAnimator.ofInt(mlp.bottomMargin, 0);
+ anim.addUpdateListener(
+ new MarginTwiddler(mMainView, new BottomMarginSetter()));
+ } else if (alp.topMargin > 0) {
+ anim = ValueAnimator.ofInt(alp.topMargin, 0);
+ anim.addUpdateListener(
+ new MarginTwiddler(mAuxView, new TopMarginSetter()));
+ }
+
+ if (anim != null) {
+ anim.setDuration(EXPAND_DURATION);
+ anim.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {}
+
+ @Override
+ public void onAnimationCancel(Animator animation) {}
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {}
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAuxView.setVisibility(View.GONE);
+ }
+ });
+
+ List<Animator> animations = getColorAnimations(mColorSelected, mColor);
+ animations.addAll(getShadowAnimations(false));
+ animations.add(anim);
+
+ AnimatorSet set = new AnimatorSet();
+ set.playTogether(animations);
+ set.setDuration(EXPAND_DURATION);
+ set.start();
+ } else {
+ // layouts are already collapsed. reset colors/visibility for completeness
+ mAuxView.setVisibility(View.GONE);
+ resetColors();
+ }
+ }
+
+ /**
+ * Interpolate color change over time
+ *
+ * @param fromColor
+ * @param toColor
+ * @return
+ */
+ private List<Animator> getColorAnimations(int fromColor, int toColor) {
+ ObjectAnimator colorAnim1 =
+ ObjectAnimator.ofInt(mMainView, "backgroundColor", fromColor, toColor);
+ colorAnim1.setEvaluator(new ArgbEvaluator());
+
+ ObjectAnimator colorAnim2 =
+ ObjectAnimator.ofInt(mAuxView, "backgroundColor", fromColor, toColor);
+ colorAnim2.setEvaluator(new ArgbEvaluator());
+
+ List<Animator> result = new ArrayList<Animator>();
+ result.add(colorAnim1);
+ result.add(colorAnim2);
+ return result;
+ }
+
+ /**
+ * Gradually decrease the margins of the shadow view so that the shadow grows over
+ * time, creating the illusion that the card is lifting up out of the view.
+ *
+ * @param isExpand
+ * @return
+ */
+ private List<Animator> getShadowAnimations(boolean isExpand) {
+ int fromTop, toTop, fromBottom, toBottom;
+ if (isExpand) {
+ fromTop = mShadowMarginTop;
+ toTop = 0;
+ fromBottom = mShadownMarginBottom;
+ toBottom = 0;
+ } else {
+ fromTop = 0;
+ toTop = mShadowMarginTop;
+ fromBottom = 0;
+ toBottom = mShadownMarginBottom;
+ }
+
+ List<Animator> result = new ArrayList<Animator>();
+
+ ValueAnimator topAnim = ValueAnimator.ofInt(fromTop, toTop);
+ topAnim.addUpdateListener(new MarginTwiddler(mShadowView, new TopMarginSetter()));
+ result.add(topAnim);
+
+ ValueAnimator bottomAnim = ValueAnimator.ofInt(fromBottom, toBottom);
+ bottomAnim.addUpdateListener(new MarginTwiddler(mShadowView, new BottomMarginSetter()));
+ result.add(bottomAnim);
+
+ return result;
+ }
+
+ /**
+ * Puts card back in original collapsed state
+ */
+ public void reset() {
+ resetColors();
+ mAuxView.setVisibility(View.GONE);
+ new BottomMarginSetter().setMargin(mMainView, 0);
+ new TopMarginSetter().setMargin(mAuxView, 0);
+ new BottomMarginSetter().setMargin(mShadowView, mShadownMarginBottom);
+ new TopMarginSetter().setMargin(mShadowView, mShadowMarginTop);
+ mAuxTop = -1;
+ mMainBottom = -1;
+ }
+
+ private void resetColors() {
+ mMainView.setBackgroundColor(mColor);
+ mAuxView.setBackgroundColor(mColor);
+ }
+
+ private class MarginTwiddler implements ValueAnimator.AnimatorUpdateListener {
+
+ private View mView;
+ private MarginSetter mMarginSetter;
+
+ public MarginTwiddler(View view, MarginSetter m) {
+ mView = view;
+ mMarginSetter = m;
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ int val = (Integer)animation.getAnimatedValue();
+ mMarginSetter.setMargin(mView, val);
+ mExpandingCard.requestLayout();
+ }
+ }
+
+ private int getAuxTop() {
+ if (mAuxTop < 0) {
+ mAuxTop = getMainBottom() - mAuxHeight;
+ }
+ return mAuxTop;
+ }
+
+ private int getMainBottom() {
+ if (mMainBottom < 0) {
+ // assumption: main view is bigger than aux view. We could use getBottom here,
+ // but getHeight seems more robust since the height never changes
+ mMainBottom = mMainView.getHeight();
+ }
+ return mMainBottom;
+ }
+
+ private static interface MarginSetter {
+ public void setMargin(View v, int margin);
+ }
+
+ private static class TopMarginSetter implements MarginSetter {
+ @Override
+ public void setMargin(View v, int margin) {
+ FrameLayout.LayoutParams lp =
+ (FrameLayout.LayoutParams)v.getLayoutParams();
+ lp.topMargin = margin;
+ v.setLayoutParams(lp);
+ }
+ }
+
+ private static class BottomMarginSetter implements MarginSetter {
+ @Override
+ public void setMargin(View v, int margin) {
+ FrameLayout.LayoutParams lp =
+ (FrameLayout.LayoutParams)v.getLayoutParams();
+ lp.bottomMargin = margin;
+ v.setLayoutParams(lp);
+ }
+ }
+
+ /**
+ * Helper class that can be used by the adapter to manage the state of expanding
+ * cards. It tracks the position of the currently selected card, and adds a click
+ * handler to manage expanding/collpasing.
+ */
+ public static class ExpandingCardManager {
+ private int mSelectedPosition = -1;
+ private ExpandingCard mSelectedCard;
+ private ListView mList;
+
+ public ExpandingCardManager(ListView list) {
+ mList = list;
+ }
+
+ /**
+ * Invoke this method whenever an expanding card view is bound to a certain position
+ * in the list
+ *
+ * @param card
+ * @param position
+ */
+ public void onBindExpandingCard(final ExpandingCard card, final int position) {
+ card.reset();
+ card.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (position == mSelectedPosition) {
+ card.collapse();
+ mSelectedPosition = -1;
+ mSelectedCard = null;
+ } else {
+ // when the selection is moved from one card to another, we want the
+ // newly selected card to expand into the space left by the collpasing
+ // one.
+ if (mSelectedPosition >= 0 && position > mSelectedPosition) {
+ card.expand(AnimationType.ANCHOR_BOTTOM);
+ } else {
+ card.expand(AnimationType.ANCHOR_TOP);
+ }
+
+ // If the currently selected card is in view, animate it closing.
+ // We're assuming that our reference to the selected card view is still
+ // valid is long as it is visible.
+ if (mSelectedPosition >= mList.getFirstVisiblePosition() &&
+ mSelectedPosition <= mList.getLastVisiblePosition() &&
+ mSelectedCard != null) {
+ mSelectedCard.collapse();
+ }
+
+ mSelectedPosition = position;
+ mSelectedCard = card;
+ }
+ }
+ });
+ if (position == mSelectedPosition) {
+ // selected card is coming back into view on a scroll, show selected
+ // state without animation
+ mSelectedCard = card;
+ card.expand(AnimationType.NONE);
+ }
+ }
+ }
+}