/* * ****************************************************************************** * Copyright (c) 2013-2014 Gabriele Mariotti. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ***************************************************************************** */ package it.gmariotti.cardslib.library.view; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.ListAdapter; import android.widget.ListView; import it.gmariotti.cardslib.library.R; import it.gmariotti.cardslib.library.internal.Card; import it.gmariotti.cardslib.library.internal.CardArrayAdapter; import it.gmariotti.cardslib.library.internal.CardCursorAdapter; import it.gmariotti.cardslib.library.view.base.CardViewWrapper; import it.gmariotti.cardslib.library.view.listener.SwipeOnScrollListener; /** * Card List View. * It uses an {@link CardArrayAdapter} to populate items. * </p> * Usage: * <pre><code> * <it.gmariotti.cardslib.library.view.CardListView * android:layout_width="match_parent" * android:layout_height="match_parent" * android:id="@+id/listId" * card:list_card_layout_resourceID="@layout/list_card_thumbnail_layout" /> * * </code></pre> * It provides a default layout id for each row @layout/list_card_layout * Use can easily customize it using card:list_card_layout_resourceID attr in your xml layout. * </p> * Use this code to populate the list view * <pre><code> * CardListView listView = (CardListView) getActivity().findViewById(R.id.listId); * listView.setAdapter(mCardArrayAdapter); * </code></pre> * </p> * Currently you have to use the same inner layout for each card in listView. * </p> * @author Gabriele Mariotti (gabri.mariotti@gmail.com) */ public class CardListView extends ListView implements CardViewWrapper.OnExpandListAnimatorListener { protected static String TAG = "CardListView"; /** * Card Array Adapter */ protected CardArrayAdapter mAdapter; /** * Card Cursor Adapter */ protected CardCursorAdapter mCursorAdapter; /** * Custom ScrollListener to be used with a CardListView and cards with swipe action */ protected SwipeOnScrollListener mOnScrollListener; //-------------------------------------------------------------------------- // Custom Attrs //-------------------------------------------------------------------------- /** * Default layout to apply to card */ protected int list_card_layout_resourceID = R.layout.list_card_layout; //-------------------------------------------------------------------------- // Constructors //-------------------------------------------------------------------------- public CardListView(Context context) { super(context); init(null, 0); } public CardListView(Context context, AttributeSet attrs) { super(context, attrs); init(attrs, 0); } public CardListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(attrs, defStyle); } //-------------------------------------------------------------------------- // Init //-------------------------------------------------------------------------- /** * Initialize * * @param attrs * @param defStyle */ protected void init(AttributeSet attrs, int defStyle){ //Init attrs initAttrs(attrs,defStyle); //Set divider to 0dp setDividerHeight(0); } /** * Init custom attrs. * * @param attrs * @param defStyle */ protected void initAttrs(AttributeSet attrs, int defStyle) { list_card_layout_resourceID = R.layout.list_card_layout; TypedArray a = getContext().getTheme().obtainStyledAttributes( attrs, R.styleable.card_options, defStyle, defStyle); try { list_card_layout_resourceID = a.getResourceId(R.styleable.card_options_list_card_layout_resourceID, this.list_card_layout_resourceID); } finally { a.recycle(); } } //-------------------------------------------------------------------------- // Adapter //-------------------------------------------------------------------------- /** * Set the adapter. You can provide a {@link CardArrayAdapter}, or a {@link it.gmariotti.cardslib.library.internal.CardCursorAdapter} * or a generic adapter. * Pay attention: your generic adapter has to call {@link CardArrayAdapter#getView} method * * @param adapter */ @Override public void setAdapter(ListAdapter adapter) { if (adapter instanceof CardArrayAdapter){ setAdapter((CardArrayAdapter)adapter); }else if (adapter instanceof CardCursorAdapter){ setAdapter((CardCursorAdapter)adapter); }else { Log.w(TAG,"You are using a generic adapter. Pay attention: your adapter has to call cardArrayAdapter#getView method" ); super.setAdapter(adapter); } } /** * Set {@link CardArrayAdapter} and layout used by items in ListView * * @param adapter {@link CardArrayAdapter} */ public void setAdapter(CardArrayAdapter adapter) { super.setAdapter(adapter); //Set Layout used by items adapter.setRowLayoutId(list_card_layout_resourceID); adapter.setCardListView(this); mAdapter=adapter; } /** * Set {@link CardCursorAdapter} and layout used by items in ListView * * @param adapter {@link CardCursorAdapter} */ public void setAdapter(CardCursorAdapter adapter) { super.setAdapter(adapter); //Set Layout used by items adapter.setRowLayoutId(list_card_layout_resourceID); adapter.setCardListView(this); mCursorAdapter=adapter; } /** * You can use this method, if you are using external adapters. * Pay attention. The generic adapter#getView() method has to call the cardArrayAdapter#getView() method to work. * * @param adapter {@link ListAdapter} generic adapter * @param cardArrayAdapter {@link CardArrayAdapter} cardArrayAdapter */ public void setExternalAdapter(ListAdapter adapter, CardArrayAdapter cardArrayAdapter) { setAdapter(adapter); mAdapter=cardArrayAdapter; mAdapter.setCardListView(this); mAdapter.setRowLayoutId(list_card_layout_resourceID); } /** * You can use this method, if you are using external adapters. * Pay attention. The generic adapter#getView() method has to call the cardCursorAdapter#getView() method to work. * * @param adapter {@link ListAdapter} generic adapter * @param cardCursorAdapter {@link CardCursorAdapter} cardArrayAdapter */ public void setExternalAdapter(ListAdapter adapter, CardCursorAdapter cardCursorAdapter) { setAdapter(adapter); mCursorAdapter=cardCursorAdapter; mCursorAdapter.setCardListView(this); mCursorAdapter.setRowLayoutId(list_card_layout_resourceID); } /** * Returns local scroll event listener */ public OnScrollListener getOnScrollListener( ) { return this.mOnScrollListener; } /** * Overrides the set on scroll listener method and registers local reference */ @Override public void setOnScrollListener( OnScrollListener mOnScrollListener ) { super.setOnScrollListener( mOnScrollListener ); if (mOnScrollListener instanceof SwipeOnScrollListener) this.mOnScrollListener = (SwipeOnScrollListener)mOnScrollListener; } //-------------------------------------------------------------------------- // Expand and Collapse animator //-------------------------------------------------------------------------- @Override public void onExpandStart(CardViewWrapper viewCard,View expandingLayout) { boolean expandable = true; if (mCursorAdapter!=null){ expandable = mCursorAdapter.onExpandStart(viewCard); } if (expandable) ExpandCollapseHelper.animateExpanding(expandingLayout,viewCard,this); if (mCursorAdapter!=null){ mCursorAdapter.onExpandEnd(viewCard); } } @Override public void onCollapseStart(CardViewWrapper viewCard,View expandingLayout) { boolean collapsible = true; if (mCursorAdapter!=null){ collapsible = mCursorAdapter.onCollapseStart(viewCard); } if (collapsible) ExpandCollapseHelper.animateCollapsing(expandingLayout,viewCard,this); if (mCursorAdapter!=null){ mCursorAdapter.onCollapseEnd(viewCard); } } /** * Helper to animate collapse and expand animation */ private static class ExpandCollapseHelper { /** * This method expandes the view that was clicked. * * @param expandingLayout layout to expand * @param cardView cardView * @param listView listView */ public static void animateCollapsing(final View expandingLayout, final CardViewWrapper cardView,final AbsListView listView) { int origHeight = expandingLayout.getHeight(); ValueAnimator animator = createHeightAnimator(expandingLayout, origHeight, 0); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(final Animator animator) { expandingLayout.setVisibility(View.GONE); cardView.setExpanded(false);//card.setExpanded(true); notifyAdapter(listView); Card card = cardView.getCard(); if (card.getOnCollapseAnimatorEndListener()!=null) card.getOnCollapseAnimatorEndListener().onCollapseEnd(card); } }); animator.start(); } /** * This method collapse the view that was clicked. * * @param expandingLayout layout to collapse * @param cardView cardView * @param listView listView */ public static void animateExpanding(final View expandingLayout, final CardViewWrapper cardView,final AbsListView listView) { /* Update the layout so the extra content becomes visible.*/ expandingLayout.setVisibility(View.VISIBLE); View parent = (View) expandingLayout.getParent(); final int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getMeasuredWidth() - parent.getPaddingLeft() - parent.getPaddingRight(), View.MeasureSpec.AT_MOST); final int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); expandingLayout.measure(widthSpec, heightSpec); ValueAnimator animator = createHeightAnimator(expandingLayout, 0, expandingLayout.getMeasuredHeight()); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { final int listViewHeight = listView.getHeight(); final int listViewBottomPadding = listView.getPaddingBottom(); final View v = findDirectChild(expandingLayout, listView); @Override public void onAnimationUpdate(final ValueAnimator valueAnimator) { final int bottom = v.getBottom(); if (bottom > listViewHeight) { final int top = v.getTop(); if (top > 0) { listView.smoothScrollBy(Math.min(bottom - listViewHeight + listViewBottomPadding, top), 0); } } } }); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); cardView.setExpanded(true);//card.setExpanded(true); notifyAdapter(listView); Card card = cardView.getCard(); if (card.getOnExpandAnimatorEndListener()!=null) card.getOnExpandAnimatorEndListener().onExpandEnd(card); } }); animator.start(); } private static View findDirectChild(final View view, final AbsListView listView) { View result = view; View parent = (View) result.getParent(); while (parent != listView) { result = parent; parent = (View) result.getParent(); } return result; } public static ValueAnimator createHeightAnimator(final View view, final int start, final int end) { ValueAnimator animator = ValueAnimator.ofInt(start, end); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(final ValueAnimator valueAnimator) { int value = (Integer) valueAnimator.getAnimatedValue(); ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); layoutParams.height = value; view.setLayoutParams(layoutParams); } }); return animator; } /** * This method notifies the adapter after setting expand value inside cards * * @param listView */ public static void notifyAdapter(AbsListView listView){ if (listView instanceof CardListView){ CardListView cardListView = (CardListView) listView; if (cardListView.mAdapter!=null){ cardListView.mAdapter.notifyDataSetChanged(); } else if (cardListView.mCursorAdapter!=null){ //cardListView.mCursorAdapter.notifyDataSetChanged(); } } } } }