package com.afollestad.cardsui; import android.content.Context; import android.util.SparseIntArray; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.ImageView; import android.widget.PopupMenu; import android.widget.TextView; import com.afollestad.silk.adapters.SilkCursorAdapter; import com.afollestad.silk.caching.SilkCursorItem; /** * @author Aidan Follestad (afollestad) */ public class CardCursorAdapter<ItemType extends CardBase<ItemType> & SilkCursorItem<ItemType>> extends SilkCursorAdapter<ItemType> { private final static int TYPE_REGULAR = 0; private final static int TYPE_NO_CONTENT = 1; private final static int TYPE_HEADER = 2; private final static int DEFAULT_TYPE_COUNT = 3; private final static int POPUP_MENU_THEME = android.R.style.Theme_Holo_Light; private final SparseIntArray mViewTypes; protected int mAccentColor; private int mPopupMenu = -1; private Card.CardMenuListener<ItemType> mPopupListener; private boolean mCardsClickable = true; private int mLayout = R.layout.list_item_card; private int mLayoutNoContent = R.layout.list_item_card_nocontent; private final int mLayoutHeader = R.layout.list_item_header; /** * Initializes a new CardCursorAdapter instance. * * @param context The context used to inflate layouts and retrieve resources. */ public CardCursorAdapter(Context context, Class<? extends SilkCursorItem> cls) { super(context, cls); mAccentColor = context.getResources().getColor(android.R.color.black); mViewTypes = new SparseIntArray(); registerLayout(R.layout.list_item_header_centered); registerLayout(R.layout.list_item_card_compressed); } /** * Initializes a new CardCursorAdapter instance. * * @param context The context used to inflate layouts and retrieve resources. * @param cardLayoutRes Sets a custom layout to be used for all cards (not including headers) in the adapter. * This <b>does not</b> override layouts set to individual cards. */ public CardCursorAdapter(Context context, Class<? extends SilkCursorItem> cls, int cardLayoutRes) { this(context, cls); mLayout = cardLayoutRes; } /** * Initializes a new CardCursorAdapter instance. * * @param context The context used to inflate layouts and retrieve resources. * @param cardLayoutRes Sets a custom layout to be used for all cards (not including headers) in the adapter. * This <b>does not</b> override layouts set to individual cards. * @param cardLayoutNoContentRes Sets a custom layout to be used for all cards (not including headers) in the * adapter with null content. This <b>does not</b> override layouts set to individual cards. */ public CardCursorAdapter(Context context, Class<? extends SilkCursorItem> cls, int cardLayoutRes, int cardLayoutNoContentRes) { this(context, cls, cardLayoutRes); mLayoutNoContent = cardLayoutNoContentRes; } @Override public final boolean isEnabled(int position) { ItemType item = getItem(position); if (!mCardsClickable && !item.isHeader()) return false; if (item.isHeader()) return item.getActionCallback() != null; return item.isClickable(); } /** * Sets the accent color used on card titles and header action buttons. * You <b>should</b> call this method before adding any cards to the adapter to avoid issues. * * @param color The resolved color to use as an accent. */ public final CardCursorAdapter<ItemType> setAccentColor(int color) { mAccentColor = color; return this; } /** * Sets the accent color resource used on card titles and header action buttons. * You <b>should</b> call this method before adding any cards to the adapter to avoid issues. * * @param colorRes The color resource ID to use as an accent. */ public final CardCursorAdapter<ItemType> setAccentColorRes(int colorRes) { setAccentColor(getContext().getResources().getColor(colorRes)); return this; } /** * Sets a popup menu used for every card in the adapter, this will not override individual card popup menus. * You <b>should</b> call this method before adding any cards to the adapter to avoid issues. * * @param menuRes The menu resource ID to use for the card's popup menu. * @param listener A listener invoked when an option in the popup menu is tapped by the user. */ public final CardCursorAdapter<ItemType> setPopupMenu(int menuRes, Card.CardMenuListener<ItemType> listener) { mPopupMenu = menuRes; mPopupListener = listener; return this; } protected Card.CardMenuListener<ItemType> getMenuListener() { return mPopupListener; } /** * Sets whether or not cards in the adapter are clickable, setting it to false will turn card's list selectors off * and the list's OnItemClickListener will not be called. This <b>will</b> override individual isClickable values * set to {@link Card}s. */ public final CardCursorAdapter<ItemType> setCardsClickable(boolean clickable) { mCardsClickable = clickable; return this; } @Override public final int getLayout(int index, int type) { CardBase card = getItem(index); if (type == TYPE_NO_CONTENT) return mLayoutNoContent; else if (type == TYPE_HEADER) return R.layout.list_item_header; int layout = card.getLayout(); if (layout <= 0) { // If no layout was specified for the individual card, use the adapter's set layout layout = mLayout; } return layout; } private void setupHeader(ItemType header, View view) { TextView title = (TextView) view.findViewById(android.R.id.title); if (title == null) throw new RuntimeException("Your header layout must contain a TextView with the ID @android:id/title."); TextView subtitle = (TextView) view.findViewById(android.R.id.content); if (subtitle == null) throw new RuntimeException("Your header layout must contain a TextView with the ID @android:id/content."); title.setText(header.getTitle()); if (header.getContent() != null && !header.getContent().trim().isEmpty()) { subtitle.setVisibility(View.VISIBLE); subtitle.setText(header.getContent()); } else subtitle.setVisibility(View.GONE); TextView button = (TextView) view.findViewById(android.R.id.button1); if (button == null) throw new RuntimeException("The header layout must contain a TextView with the ID @android:id/button1."); if (header.getActionCallback() != null) { button.setVisibility(View.VISIBLE); button.setBackgroundColor(mAccentColor); String titleTxt = header.getActionTitle(); if (header.getActionTitle() == null || header.getActionTitle().trim().isEmpty()) titleTxt = getContext().getString(R.string.see_more); button.setText(titleTxt); } else button.setVisibility(View.GONE); } private void invalidatePadding(int index, View view) { int top = index == 0 ? R.dimen.card_outer_padding_firstlast : R.dimen.card_outer_padding_top; int bottom = index == (getCount() - 1) ? R.dimen.card_outer_padding_firstlast : R.dimen.card_outer_padding_top; view.setPadding(view.getPaddingLeft(), getContext().getResources().getDimensionPixelSize(top), view.getPaddingRight(), getContext().getResources().getDimensionPixelSize(bottom)); } @Override public View onViewCreated(int index, View recycled, ItemType item) { if (item.isHeader()) { setupHeader(item, recycled); return recycled; } TextView title = (TextView) recycled.findViewById(android.R.id.title); if (title != null) onProcessTitle(title, item, mAccentColor); TextView content = (TextView) recycled.findViewById(android.R.id.content); if (content != null) onProcessContent(content, item); ImageView icon = (ImageView) recycled.findViewById(android.R.id.icon); if (icon != null) { if (onProcessThumbnail(icon, item)) { icon.setVisibility(View.VISIBLE); } else { icon.setVisibility(View.GONE); } } View menu = recycled.findViewById(android.R.id.button1); if (menu != null) { if (onProcessMenu(menu, item)) { menu.setVisibility(View.VISIBLE); } else { menu.setOnClickListener(null); menu.setVisibility(View.INVISIBLE); } } invalidatePadding(index, recycled); return recycled; } @Override public Object getItemId(ItemType item) { return item.getSilkId(); } @Override public final int getViewTypeCount() { return mViewTypes.size() + DEFAULT_TYPE_COUNT; } /** * Registers a custom layout in the adapter, that isn't one of the default layouts, and that was passed in the adapter's constructor. * <p/> * This must be used if you override getLayout() and specify custom layouts for certain list items. */ public final CardCursorAdapter<ItemType> registerLayout(int layoutRes) { if (layoutRes == mLayout || layoutRes == mLayoutNoContent || layoutRes == mLayoutHeader) return this; mViewTypes.put(layoutRes, mViewTypes.size() + DEFAULT_TYPE_COUNT); return this; } @Override public final int getItemViewType(int position) { CardBase item = getItem(position); if (item.getLayout() > 0) { if (mViewTypes.get(item.getLayout()) != 0) return mViewTypes.get(item.getLayout()); else if (mLayout == item.getLayout()) return TYPE_REGULAR; else if (mLayoutNoContent == item.getLayout()) return TYPE_NO_CONTENT; else if (mLayoutHeader == item.getLayout()) return TYPE_HEADER; String name = getContext().getResources().getResourceName(item.getLayout()); throw new RuntimeException("The layout " + name + " is not registered."); } else { if (item.isHeader()) return TYPE_HEADER; else if ((item.getContent() == null || item.getContent().trim().isEmpty())) return TYPE_NO_CONTENT; else return TYPE_REGULAR; } } protected boolean onProcessTitle(TextView title, ItemType card, int accentColor) { if (title == null) return false; title.setText(card.getTitle()); title.setTextColor(accentColor); return true; } protected boolean onProcessThumbnail(ImageView icon, ItemType card) { if (icon == null) return false; if (card.getThumbnail() == null) return false; icon.setImageDrawable(card.getThumbnail()); return true; } protected boolean onProcessContent(TextView content, ItemType card) { content.setText(card.getContent()); return false; } protected boolean onProcessMenu(final View view, final ItemType card) { if (card.getPopupMenu() < 0) { // Menu for this card is disabled return false; } int menuRes = mPopupMenu; if (card.getPopupMenu() != 0) menuRes = card.getPopupMenu(); if (menuRes < 0) { // No menu for the adapter or the card return false; } CardAdapter.setupTouchDelegate(getContext(), view); view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int menuRes = mPopupMenu; if (card.getPopupMenu() != 0) menuRes = card.getPopupMenu(); // Force the holo light theme on every card's popup menu Context themedContext = getContext(); themedContext.setTheme(POPUP_MENU_THEME); PopupMenu popup = new PopupMenu(themedContext, view); MenuInflater inflater = popup.getMenuInflater(); inflater.inflate(menuRes, popup.getMenu()); popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { if (card.getPopupMenu() > 0 && card.getPopupListener() != null) { // This individual card has it unique menu card.getPopupListener().onMenuItemClick(card, item); } else if (mPopupListener != null) { // The card does not have a unique menu, use the adapter's default mPopupListener.onMenuItemClick(card, item); } return false; } }); popup.show(); } }); return true; } }