/*
* 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 android.content.Context;
import android.database.DataSetObserver;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.DragEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ListView;
import ch.ethz.dcg.pancho3.R;
import ch.ethz.dcg.pancho3.tablet.widget.SwipeHelper.Callback;
/**
* View which consists of a list with a header element on top that doesn't
* scroll. The view allows the dragging and swipe removing of elements.
*
* The MagicView needs a MagicListAdapter to be fully operational.
*
* @author Yannick Stucki (yannickstucki@gmail.com)
*
*/
public class MagicView extends LinearLayout implements Callback {
private static final int ITEM_HEIGHT = 64;
// The header wraps one item and is always on top (doesn't scroll).
private final FrameLayout magicHeader;
private final View nowPlayingView;
// The rect will be used for hitting calculations.
private final Rect rect = new Rect();
// The list view contains the rest of the items.
private final MagicListView listView;
// A magic view needs a magic adapter.
private MagicListAdapter<?> magicListAdapter;
// Dragging is the process of reordering items or swiping them to be
// removed.
// True while dragging occurs
private boolean isDragging = false;
private final FrameLayout draggedView;
private View draggedViewContent;
private int draggedYOffset;
private int draggedViewY;
private int lastPosition = -1;
private int firstYCoordinate = 0;
private OnItemClickListener listener;
private int lastY;
private final int itemHeight;
private SwipeHelper swipeHelper;
/**
* The constructor.
*/
public MagicView(Context context, AttributeSet attrs) {
super(context, attrs);
nowPlayingView = LayoutInflater.from(context).inflate(R.layout.tablet_nowplaying, null);
magicHeader = (FrameLayout) nowPlayingView.findViewById(R.id.headercontainer);
this.addView(nowPlayingView);
listView = new MagicListView(context);
listView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
this.addView(listView);
draggedView = new FrameLayout(context);
this.addView(draggedView);
draggedView.setBackgroundColor(context.getResources().getColor(R.color.trans_dark));
itemHeight = Math.round(context.getResources().getDisplayMetrics().density * ITEM_HEIGHT);
float densityScale = getResources().getDisplayMetrics().density;
float pagingTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
swipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop);
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (listener != null) {
listener.onItemClick(parent, view, position + 1, id);
}
}
});
magicHeader.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
listener.onItemClick(listView, magicHeader, 0, 0);
}
});
}
public void setOnItemClickListener(OnItemClickListener listener) {
this.listener = listener;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (isDragging) {
layoutDraggedView();
} else {
draggedView.layout(0, 0, 0, 0);
}
}
/**
* Sets the adapter that handles all the magic that happens in this view.
* Without this adapter, this view is not fully operational.
*/
public void setAdapter(MagicListAdapter<?> magicListAdapter) {
listView.setAdapter(magicListAdapter);
this.magicListAdapter = magicListAdapter;
magicListAdapter.registerDataSetObserver(new MagicDataSetObserver());
magicListAdapter.notifyDataSetChanged();
}
@Override
public boolean onDragEvent(DragEvent event) {
if (event.getAction() == DragEvent.ACTION_DRAG_LOCATION) {
return magicListAdapter.onDragEventLocation(event, getPosition((int) event.getY(), true));
}
return magicListAdapter.onDragEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (!listView.scrolling && !isDragging && swipeHelper.onInterceptTouchEvent(event)) {
return true;
}
if (isDragEvent(event)) {
final int y = (int) event.getY();
int position = getPosition(y, true);
if (position < 0) {
// If we get an invalid position we just use the last position.
position = lastPosition;
}
if (position >= 0) { // -1 would be an invalid value.
lastPosition = position;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startDragging(y, position);
return true;
}
}
}
return false;
}
/**
* This implementation overrides the default behavior if the dragging
* handles are touched and initiates dragging and removing by swiping.
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!listView.scrolling && !isDragging && swipeHelper.onTouchEvent(event)) {
return true;
}
// We intercept this touch event and handle the dragging if it is a drag
// event.
if (isDragEvent(event)) {
final int y = (int) event.getY();
int position = getPosition(y, true);
if (position < 0) {
// If we get an invalid position we just use the last position.
position = lastPosition;
}
if (position >= 0) { // -1 would be an invalid value.
lastPosition = position;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startDragging(y, position);
break;
case MotionEvent.ACTION_MOVE:
draggedViewY = y - draggedYOffset;
layoutDraggedView();
// We're in the process of dragging
magicListAdapter.continueDragging(position);
// If we're towards the top or the bottom of the screen
// while dragging, we need to scroll.
// TODO: Improve this, why does it work with only 0
// remove magic numbers.
if (y < 150 && y < firstYCoordinate - 50) {
listView.awakenScrollBars();
listView.setSelectionFromTop(0, listView
.getChildAt(0).getTop() + 10);
} else if (y > this.getHeight() - 100
&& y > firstYCoordinate + 50) {
listView.awakenScrollBars();
listView.setSelectionFromTop(0, listView
.getChildAt(0).getTop() - 10);
}
break;
default: // CANCEL
// Dragging is over.
magicListAdapter.stopDragging();
isDragging = false;
break;
}
}
}
// We always return true as we need to receive all the touch events.
return true;
}
private void startDragging(int y, int position) {
// We need an item in the queue so dragging makes sense
// "> 0" because the header is not counted in this getCount
if (!isDragging && magicListAdapter.getCount() > 0) {
draggedYOffset = getYOffset(y, position);
isDragging = true;
magicListAdapter.startDragging(position);
updateDraggedView();
firstYCoordinate = y;
}
}
// TODO: magic number.
private boolean isDragEvent(MotionEvent event) {
return magicListAdapter != null && event.getX() > getWidth() - itemHeight && event.getAction() ==
MotionEvent.ACTION_DOWN || isDragging;
}
private int getYOffset(int y, int position) {
if (position == 0) {
return y;
}
// TODO: explain this and verify that it always works
return y - listView.getChildAt(position - 1 - listView.getFirstVisiblePosition())
.getTop() - listView.getTop();
}
private void updateDraggedView() {
draggedViewContent = magicListAdapter
.getDraggedView(draggedViewContent);
draggedView.removeAllViews();
// TODO: we had once a nullpointer here
draggedView.addView(draggedViewContent);
}
private void layoutDraggedView() {
draggedView.layout(0, draggedViewY, this.getMeasuredWidth(), draggedViewY + itemHeight);
draggedViewContent.layout(0, 0, this.getMeasuredWidth(), itemHeight);
}
// Returns the position of the element which is y pixels from the top.
// 0 is the head element, from 1 on the elements are in the list.
int getPosition(int y, boolean deduceHeader) {
int position = 0;
nowPlayingView.getHitRect(rect);
// If the point is the rect, we leave the position at 0.
if (!rect.contains(0, y) || !deduceHeader) {
int realY = y;
if (deduceHeader) {
realY -= nowPlayingView.getBottom();
}
position = listView.pointToPosition(0, realY);
if (position >= 0) {
// If it's a valid position, we increase it by one, since the
// list starts at 1.
position++;
}
}
return position;
}
// We need our own DataSetObserver so we can update the header.
private class MagicDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
if (magicListAdapter != null) {
View view = null;
if (magicHeader.getChildCount() > 0) {
view = magicHeader.getChildAt(0);
}
if (magicListAdapter.hasHeader()) {
View newView = magicListAdapter.getHeaderView(view);
magicHeader.removeAllViews();
magicHeader.addView(newView);
} else {
magicHeader.removeAllViews();
}
}
}
}
@Override
public View getChildAtPosition(MotionEvent event) {
int position;
final int y = (int) event.getY();
final int headerHeight = nowPlayingView.getBottom();
if (y < headerHeight) {
if (y < itemHeight) {
return magicHeader.getChildAt(0);
}
} else {
position = listView.pointToPosition((int) event.getX(), y - headerHeight);
for (int i = 0; i < listView.getChildCount(); i++) {
View view = listView.getChildAt(i);
if (view instanceof QueueItem) {
QueueItem item = (QueueItem) view;
if (item.position == position + 1) {
return view;
}
}
}
}
return null;
}
@Override
public View getChildContentView(View v) {
return v;
}
@Override
public boolean canChildBeDismissed(View v) {
return true;
}
@Override
public void onBeginDrag(View v) {
// We do this so the underlying ScrollView knows that it won't get
// the chance to intercept events anymore
requestDisallowInterceptTouchEvent(true);
v.setActivated(true);
}
@Override
public void onChildDismissed(View v) {
v.setTag(Boolean.TRUE);
magicListAdapter.removeQueueItem((QueueItem) v);
}
@Override
public void onDragCancelled(View v) {
v.setActivated(false);
}
// The list view which displays elements 1 to n.
private class MagicListView extends ListView {
protected boolean scrolling = false;
public MagicListView(Context context) {
super(context);
setOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
scrolling = false;
} else {
scrolling = true;
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
}
});
}
/**
* This implementation doesn't handle the touches where the MagicView
* needs to do the dragging.
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
final int y = (int) event.getY();
// We don't handle drag events.
if (isDragEvent(event)) {
event.setLocation(event.getX(), event.getY() + nowPlayingView.getBottom());
MagicView.this.onTouchEvent(event);
return true;
}
lastY = y;
return super.onTouchEvent(event);
}
/**
* Made this public (from protected) so it can be used in the MagicView.
*/
@Override
public boolean awakenScrollBars() {
return super.awakenScrollBars();
}
}
}