/*******************************************************************************
* Copyright 2012 Crazywater
*
* 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 de.knufficast.ui;
import java.util.HashSet;
import java.util.Set;
import android.content.Context;
import android.database.DataSetObserver;
import android.util.AttributeSet;
import android.view.DragEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.WrapperListAdapter;
/**
* A {@link ListView} that supports drag and drop by long-pressing items.
*
* @author crazywater
*
*/
public class DnDListView extends ListView {
private Listener listener;
private boolean dragging;
private boolean inside;
private int dragIndex;
private int dropIndex;
private ListAdapter adapter;
private final FakeAdapter fakeAdapter = new FakeAdapter();
public DnDListView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
this.setOnDragListener(myDragEventListener);
this.setOnItemClickListener(myClickListener);
this.setOnItemLongClickListener(myLongClickListener);
}
/**
* Set the listener to be notified upon reorder and remove events.
*/
public void setListener(Listener listener) {
this.listener = listener;
}
/**
* Wraps around the real adapter set by {@link #setAdapter}: Displays the
* reordered items if the user is currently dragging one.
*
* @author crazywater
*
*/
private class FakeAdapter implements WrapperListAdapter {
@Override
public boolean areAllItemsEnabled() {
return adapter.areAllItemsEnabled();
}
@Override
public boolean isEnabled(int position) {
return adapter.isEnabled(position);
}
@Override
public int getCount() {
if (dragging && !inside) {
return adapter.getCount() - 1;
}
return adapter.getCount();
}
@Override
public Object getItem(int position) {
return adapter.getItem(position);
}
@Override
public long getItemId(int position) {
return adapter.getItemId(position);
}
@Override
public int getItemViewType(int position) {
return adapter.getItemViewType(position);
}
@Override
public int getViewTypeCount() {
return adapter.getViewTypeCount();
}
@Override
public boolean hasStableIds() {
return adapter.hasStableIds();
}
@Override
public boolean isEmpty() {
return adapter.isEmpty();
}
private Set<DataSetObserver> observers = new HashSet<DataSetObserver>();
@Override
public void registerDataSetObserver(DataSetObserver observer) {
observers.add(observer);
adapter.registerDataSetObserver(observer);
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
observers.remove(observer);
adapter.unregisterDataSetObserver(observer);
}
@Override
public ListAdapter getWrappedAdapter() {
return adapter;
}
private void notifyObservers() {
for (DataSetObserver observer : observers) {
observer.onChanged();
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
int ourPosition = position;
// pretend to drop at Integer.MAX_VALUE if we're not inside for
// computations
int ourDropIndex = inside ? dropIndex : Integer.MAX_VALUE;
if (dragging) {
if (position == ourDropIndex) {
// exactly the dragging item
ourPosition = dragIndex;
} else {
if (ourPosition > ourDropIndex) {
// shift all items one down
ourPosition--;
}
if (ourPosition >= dragIndex) {
// shift all items one up
ourPosition++;
}
}
}
return adapter.getView(ourPosition, convertView, parent);
}
};
@Override
public void setAdapter(final ListAdapter adapter) {
this.adapter = adapter;
super.setAdapter(fakeAdapter);
}
private OnItemLongClickListener myLongClickListener = new OnItemLongClickListener() {
public boolean onItemLongClick(AdapterView<?> arg0, View v, int position,
long arg3) {
dragging = true;
inside = true;
dragIndex = position;
dropIndex = position;
v.startDrag(null, new DragShadowBuilder(v), null, 0);
return true;
}
};
private OnItemClickListener myClickListener = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int position,
long arg3) {
listener.click(position);
}
};
private View.OnDragListener myDragEventListener = new View.OnDragListener() {
@Override
public boolean onDrag(View v, DragEvent event) {
final int action = event.getAction();
switch(action) {
case DragEvent.ACTION_DRAG_STARTED:
fakeAdapter.notifyObservers();
return true;
case DragEvent.ACTION_DRAG_ENTERED:
inside = true;
fakeAdapter.notifyObservers();
return true;
case DragEvent.ACTION_DRAG_LOCATION:
int x = (int) event.getX();
int y = (int) event.getY();
int newDropIndex = pointToPosition(x, y);
if (newDropIndex == AdapterView.INVALID_POSITION) {
return false;
}
if (dropIndex != newDropIndex) {
dropIndex = newDropIndex;
fakeAdapter.notifyObservers();
}
return true;
case DragEvent.ACTION_DRAG_EXITED:
inside = false;
fakeAdapter.notifyObservers();
return true;
case DragEvent.ACTION_DROP:
return true;
case DragEvent.ACTION_DRAG_ENDED:
dragging = false;
if (inside) {
listener.drop(dragIndex, dropIndex);
} else {
listener.remove(dragIndex);
};
return true;
default:
return false;
}
};
};
/**
* Interface the listener has to implement.
*
* @author crazywater
*
*/
public interface Listener {
void drop(int from, int to);
void click(int which);
void remove(int which);
}
}