/* * Copyright 2008-2013, ETH Zürich, Samuel Welten, Michael Kuhn, Tobias Langner, * Sandro Affentranger, Lukas Bossard, Michael Grob, Rahul Jain, * Dominic Langenegger, Sonia Mayor Alonso, Roger Odermatt, Tobias Schlueter, * Yannick Stucki, Sebastian Wendland, Samuel Zehnder, Samuel Zihlmann, * Samuel Zweifel * * This file is part of Jukefox. * * Jukefox is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or any later version. Jukefox is * distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * Jukefox. If not, see <http://www.gnu.org/licenses/>. */ package ch.ethz.dcg.pancho3.tablet.widget; import java.util.List; import android.content.Context; import android.database.DataSetObserver; import android.view.DragEvent; import android.view.View; import android.view.ViewGroup; import android.widget.ListAdapter; import android.widget.TextView; import ch.ethz.dcg.pancho3.R; import ch.ethz.dcg.pancho3.tablet.view.DragManager; import ch.ethz.dcg.pancho3.tablet.view.DragManager.DragDataContainer; /** * A list adapter for the MagicView. * * @author Yannick Stucki (yannickstucki@gmail.com) */ public class MagicListAdapter<T> implements ListAdapter { // Listener needed for a callback to add a new item to this adapter. public static interface NewItemListener { void onRequestNewItem(); } // Interface for the inner adapter. public static interface MagicListInnerAdapter<T> extends ListAdapter { /** * Appends an item at the end. */ void appendItem(T object); /** * Removes an item at the specified position. */ void removeItem(int position); /** * Called when the underlying data changed. */ void notifyDataSetChanged(); /** * Moves an item from the startPosition to the endPosition. */ void moveItem(int startPosition, int endPosition); /** * Moves an item from the startPosition to the endPosition. */ void insertItemsAndRemoveLast(List<T> items, int insertPosition); /** * Returns the data of the item displayed at the specified position. */ T getItem(int position); /** * Returns the view which should be displayed at the specified position, * but for an element which is currently being dragged. */ View getDraggingView(int position, View convertView, ViewGroup parent); } private static final int ITEM_HEIGHT = 64; // The inner adapter holds the data for all the items. // The item at position 0 represents the item in the header, while // the following items represent the items in the list. // This class (MagicListAdapter) // reroutes the calls from the list to only give it access to item // 1 to n. private final MagicListInnerAdapter<T> innerAdapter; // Dragging is the process of reordering items or swiping them to be // removed. // While we're dragging an item, this is true. private boolean isDragging = false; private boolean isRemoving = false; private int removePosition; // The position when the drag started if isDragging. private int dragStartPosition; // The position where the drag currently is if isDragging. private int dragCurrentPosition; private final TextView emptyViewForHeader; private final TextView emptyViewForList; // A listener which reacts to the new item request private final NewItemListener newItemListener; // TODO: can this be calculated in a more elegant way? private final int itemHeight; private final DragManager dragManager; /** * The constructor needs a MagicListInnerAdapter. */ public MagicListAdapter(Context context, MagicListInnerAdapter<T> innerAdapter, NewItemListener newItemListener, DragManager dragManager) { this.innerAdapter = innerAdapter; this.newItemListener = newItemListener; this.dragManager = dragManager; emptyViewForHeader = new TextView(context); emptyViewForHeader.setBackgroundResource(R.drawable.d170_item_highlight); emptyViewForList = new TextView(context); emptyViewForList.setBackgroundResource(R.drawable.d170_item_highlight); itemHeight = Math.round(context.getResources().getDisplayMetrics().density * ITEM_HEIGHT); } /** * Adds the specified item to the end of the list. */ public void add(T item) { innerAdapter.appendItem(item); innerAdapter.notifyDataSetChanged(); } /** * Called when the dragging starts at the specified position. */ public void startDragging(int position) { dragStartPosition = position; dragCurrentPosition = position; isDragging = true; innerAdapter.notifyDataSetChanged(); } public void startRemoving(int position) { removePosition = position; isRemoving = true; innerAdapter.notifyDataSetChanged(); } public void stopRemoving() { isRemoving = false; innerAdapter.notifyDataSetChanged(); } /** * Called when the touch events come in and the view is in drag mode. * * @param position * the current position where the dragged element is. */ public void continueDragging(int position) { if (dragCurrentPosition != position) { // Only update if the current position changed. TODO: maybe check // this in the MagicView? dragCurrentPosition = position; innerAdapter.notifyDataSetChanged(); } } /** * Called when dragging stops. */ public void stopDragging() { // TODO: maybe we need some write lock here? But probably not needed. isDragging = false; innerAdapter.moveItem(dragStartPosition, dragCurrentPosition); innerAdapter.notifyDataSetChanged(); } public void stopDraggingAfterInsert() { isDragging = false; innerAdapter.insertItemsAndRemoveLast(lastDragDataContainer.getData(), dragCurrentPosition); innerAdapter.notifyDataSetChanged(); } // Remove the element at the specified position. // (dragging mode not included and will be turned off). public void remove(int position) { isDragging = false; innerAdapter.removeItem(position); innerAdapter.notifyDataSetChanged(); } public void removeQueueItem(QueueItem item) { item.dismiss(); remove(item.position); } /** * Returns a view representing the header item. Possibly reuses convertView. */ public View getHeaderView(View convertView) { if (isDragging && dragCurrentPosition == 0 || isRemoving && removePosition == 0) { emptyViewForHeader.setHeight(itemHeight); return emptyViewForHeader; } if (convertView == emptyViewForHeader) { convertView = null; } final int position; if (isDragging && dragStartPosition == 0) { // The element 0 has been dragged away, and element 1 is now on top. position = 1; } else { // The header view usually holds the object at position 0. position = 0; } View item = innerAdapter.getView(position, convertView, null); return item; } public View getDraggedView(View convertView) { if (isRemoving) { return innerAdapter.getDraggingView(removePosition, convertView, null); } return innerAdapter.getDraggingView(dragStartPosition, convertView, null); } /** * The underlying data changed and the view needs to be redrawn. */ public void notifyDataSetChanged() { innerAdapter.notifyDataSetChanged(); } public void requestNewItem() { newItemListener.onRequestNewItem(); } /** * Returns the item associated with the header of this view. */ public T getHeaderItem() { return innerAdapter.getItem(0); } // Below here are the methods which override the ListAdapter interface. // This methods don't include the head item, but only the ones represented // in the list. This methods are meant to be used by the ListView to // get the proper information about all the items that are actually in the // list. Note that positions are always mapped to other positions (see // #mapPosition). @Override public boolean areAllItemsEnabled() { return innerAdapter.areAllItemsEnabled(); } @Override public boolean isEnabled(int position) { return innerAdapter.isEnabled(mapPosition(position)); } /** * The count is one less than in the innerAdapter, since the list doesn't * see the top item. */ @Override public int getCount() { int count = innerAdapter.getCount() - 1; if (count < 0) { return 0; } return count; } @Override public T getItem(int position) { return innerAdapter.getItem(mapPosition(position)); } @Override public long getItemId(int position) { return innerAdapter.getItemId(mapPosition(position)); } @Override public int getItemViewType(int position) { if (isDragging && mapPosition(position) == dragStartPosition || isRemoving && position + 1 == removePosition) { return ListAdapter.IGNORE_ITEM_VIEW_TYPE; } return innerAdapter.getItemViewType(mapPosition(position)); } @Override public View getView(int position, View convertView, ViewGroup parent) { if (isDragging && dragCurrentPosition == position + 1 || isRemoving && removePosition == position + 1) { emptyViewForList.setHeight(itemHeight); return emptyViewForList; } return innerAdapter.getView(mapPosition(position), convertView, parent); } @Override public int getViewTypeCount() { return innerAdapter.getViewTypeCount(); } @Override public boolean hasStableIds() { return innerAdapter.hasStableIds(); } /** * One item already means that the list is empty since this item will be the * head. */ @Override public boolean isEmpty() { return innerAdapter.getCount() <= 1; } public boolean hasHeader() { return innerAdapter.getCount() > 0; } @Override public void registerDataSetObserver(DataSetObserver observer) { innerAdapter.registerDataSetObserver(observer); } @Override public void unregisterDataSetObserver(DataSetObserver observer) { innerAdapter.unregisterDataSetObserver(observer); } // Below here are private helper methods. // This method list positions to positions to use the inner adapter. This // method doesn't work for the head view, but only for the list // items. private int mapPosition(int position) { position++; // The +1 on everything is since the list starts at 1 // because of the head. if (!isDragging/* && !innerAdapter.isWaitingForActionToFinish()*/) { return position; // When we're not in drag mode, there is no other // mapping going on. } if (position < dragCurrentPosition && position < dragStartPosition || position > dragCurrentPosition && position > dragStartPosition) { // If we're below or above all the dragging action, there's also no // further mapping going on. return position; } // If we're at the position where the dragged item currently is, it is // the item which was // at the position where the dragging started. if (position == dragCurrentPosition) { return dragStartPosition; } // Otherwise the item is shifted by one position, depending on whether // the dragging. // is going up or down. if (dragCurrentPosition < dragStartPosition) { return position - 1; } else { return position + 1; } } private DragDataContainer<T> lastDragDataContainer; // Experimental TODO: Clean this stuff up. public boolean onDragEvent(DragEvent event) { if (!dragManager.onDragEvent(event)) { switch (event.getAction()) { case DragEvent.ACTION_DRAG_ENTERED: DragDataContainer<T> data = (DragDataContainer<T>) event.getLocalState(); if (data.isReady()) { lastDragDataContainer = data; add(data.getData().get(0)); startDragging(innerAdapter.getCount() - 1); } break; case DragEvent.ACTION_DRAG_LOCATION: break; case DragEvent.ACTION_DRAG_EXITED: if (lastDragDataContainer != null) { remove(innerAdapter.getCount() - 1); stopDragging(); } break; case DragEvent.ACTION_DROP: if (lastDragDataContainer != null) { stopDraggingAfterInsert(); } break; } } return true; } public boolean onDragEventLocation(DragEvent event, int position) { if (position >= 0) { continueDragging(position); } return true; } }