You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
466 lines
12 KiB
Java
466 lines
12 KiB
Java
/**
|
|
* Derived from: https://github.com/thquinn/DraggableGridView
|
|
* Chacnged inheritence to FrameLayout to be able to add custom views.
|
|
*/
|
|
//TO DO:
|
|
//
|
|
// - improve timer performance (especially on Eee Pad)
|
|
// - improve child rearranging
|
|
|
|
package com.langerhans.one.dgv;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
|
|
import android.annotation.SuppressLint;
|
|
import android.app.Activity;
|
|
import android.content.Context;
|
|
import android.graphics.Point;
|
|
import android.os.Handler;
|
|
import android.os.SystemClock;
|
|
import android.util.AttributeSet;
|
|
import android.util.DisplayMetrics;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.animation.AlphaAnimation;
|
|
import android.view.animation.Animation;
|
|
import android.view.animation.AnimationSet;
|
|
import android.view.animation.ScaleAnimation;
|
|
import android.view.animation.TranslateAnimation;
|
|
import android.widget.AdapterView.OnItemClickListener;
|
|
import android.widget.FrameLayout;
|
|
import android.widget.ImageView;
|
|
|
|
public class DraggableGridView extends FrameLayout implements View.OnTouchListener, View.OnClickListener, View.OnLongClickListener {
|
|
//layout vars
|
|
public static float childRatio = .9f;
|
|
protected int colCount, childSize, padding, dpi, scroll = 0;
|
|
protected float lastDelta = 0;
|
|
protected Handler handler = new Handler();
|
|
//dragging vars
|
|
protected int dragged = -1, lastX = -1, lastY = -1, lastTarget = -1;
|
|
protected boolean enabled = true, touching = false;
|
|
//anim vars
|
|
public static int animT = 150;
|
|
protected ArrayList<Integer> newPositions = new ArrayList<Integer>();
|
|
//listeners
|
|
protected OnRearrangeListener onRearrangeListener;
|
|
protected OnClickListener secondaryOnClickListener;
|
|
private OnItemClickListener onItemClickListener;
|
|
|
|
//CONSTRUCTOR AND HELPERS
|
|
public DraggableGridView (Context context, AttributeSet attrs) {
|
|
super(context, attrs);
|
|
setListeners();
|
|
handler.removeCallbacks(updateTask);
|
|
handler.postAtTime(updateTask, SystemClock.uptimeMillis() + 500);
|
|
setChildrenDrawingOrderEnabled(true);
|
|
|
|
DisplayMetrics metrics = new DisplayMetrics();
|
|
((Activity)context).getWindowManager().getDefaultDisplay().getMetrics(metrics);
|
|
dpi = metrics.densityDpi;
|
|
}
|
|
protected void setListeners()
|
|
{
|
|
setOnTouchListener(this);
|
|
super.setOnClickListener(this);
|
|
setOnLongClickListener(this);
|
|
}
|
|
@Override
|
|
public void setOnClickListener(OnClickListener l) {
|
|
secondaryOnClickListener = l;
|
|
}
|
|
protected Runnable updateTask = new Runnable() {
|
|
@SuppressLint("WrongCall")
|
|
public void run()
|
|
{
|
|
if (dragged != -1)
|
|
{
|
|
if (lastY < padding * 3 && scroll > 0)
|
|
scroll -= 20;
|
|
else if (lastY > getBottom() - getTop() - (padding * 3) && scroll < getMaxScroll())
|
|
scroll += 20;
|
|
}
|
|
else if (lastDelta != 0 && !touching)
|
|
{
|
|
scroll += lastDelta;
|
|
lastDelta *= .9;
|
|
if (Math.abs(lastDelta) < .25)
|
|
lastDelta = 0;
|
|
}
|
|
clampScroll();
|
|
onLayout(true, getLeft(), getTop(), getRight(), getBottom());
|
|
|
|
handler.postDelayed(this, 25);
|
|
}
|
|
};
|
|
|
|
//OVERRIDES
|
|
@Override
|
|
public void addView(View child) {
|
|
super.addView(child);
|
|
newPositions.add(-1);
|
|
};
|
|
@Override
|
|
public void removeViewAt(int index) {
|
|
super.removeViewAt(index);
|
|
newPositions.remove(index);
|
|
};
|
|
|
|
//LAYOUT
|
|
@Override
|
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
|
//compute width of view, in dp
|
|
float w = (r - l) / (dpi / 160f);
|
|
|
|
//determine number of columns, at least 2
|
|
colCount = 2;
|
|
int sub = 240;
|
|
w -= 280;
|
|
while (w > 0)
|
|
{
|
|
colCount++;
|
|
w -= sub;
|
|
sub += 40;
|
|
}
|
|
|
|
//determine childSize and padding, in px
|
|
childSize = (r - l) / colCount;
|
|
childSize = Math.round(childSize * childRatio);
|
|
padding = ((r - l) - (childSize * colCount)) / (colCount + 1);
|
|
|
|
for (int i = 0; i < getChildCount(); i++)
|
|
if (i != dragged)
|
|
{
|
|
Point xy = getCoorFromIndex(i);
|
|
getChildAt(i).layout(xy.x, xy.y, xy.x + childSize, xy.y + childSize);
|
|
}
|
|
}
|
|
@Override
|
|
protected int getChildDrawingOrder(int childCount, int i) {
|
|
if (dragged == -1)
|
|
return i;
|
|
else if (i == childCount - 1)
|
|
return dragged;
|
|
else if (i >= dragged)
|
|
return i + 1;
|
|
return i;
|
|
}
|
|
public int getIndexFromCoor(int x, int y)
|
|
{
|
|
int col = getColOrRowFromCoor(x), row = getColOrRowFromCoor(y + scroll);
|
|
if (col == -1 || row == -1) //touch is between columns or rows
|
|
return -1;
|
|
int index = row * colCount + col;
|
|
if (index >= getChildCount())
|
|
return -1;
|
|
return index;
|
|
}
|
|
protected int getColOrRowFromCoor(int coor)
|
|
{
|
|
int coor_local = coor;
|
|
coor_local -= padding;
|
|
for (int i = 0; coor_local > 0; i++)
|
|
{
|
|
if (coor_local < childSize)
|
|
return i;
|
|
coor_local -= (childSize + padding);
|
|
}
|
|
return -1;
|
|
}
|
|
protected int getTargetFromCoor(int x, int y)
|
|
{
|
|
if (getColOrRowFromCoor(y + scroll) == -1) //touch is between rows
|
|
return -1;
|
|
//if (getIndexFromCoor(x, y) != -1) //touch on top of another visual
|
|
//return -1;
|
|
|
|
int leftPos = getIndexFromCoor(x - (childSize / 4), y);
|
|
int rightPos = getIndexFromCoor(x + (childSize / 4), y);
|
|
if (leftPos == -1 && rightPos == -1) //touch is in the middle of nowhere
|
|
return -1;
|
|
if (leftPos == rightPos) //touch is in the middle of a visual
|
|
return -1;
|
|
|
|
int target = -1;
|
|
if (rightPos > -1)
|
|
target = rightPos;
|
|
else if (leftPos > -1)
|
|
target = leftPos + 1;
|
|
if (dragged < target)
|
|
return target - 1;
|
|
|
|
//Toast.makeText(getContext(), "Target: " + target + ".", Toast.LENGTH_SHORT).show();
|
|
return target;
|
|
}
|
|
protected Point getCoorFromIndex(int index)
|
|
{
|
|
int col = index % colCount;
|
|
int row = index / colCount;
|
|
return new Point(padding + (childSize + padding) * col,
|
|
padding + (childSize + padding) * row - scroll);
|
|
}
|
|
public int getIndexOf(View child)
|
|
{
|
|
for (int i = 0; i < getChildCount(); i++)
|
|
if (getChildAt(i) == child)
|
|
return i;
|
|
return -1;
|
|
}
|
|
|
|
//EVENT HANDLERS
|
|
public void onClick(View view) {
|
|
if (enabled)
|
|
{
|
|
if (secondaryOnClickListener != null)
|
|
secondaryOnClickListener.onClick(view);
|
|
if (onItemClickListener != null && getLastIndex() != -1)
|
|
onItemClickListener.onItemClick(null, getChildAt(getLastIndex()), getLastIndex(), getLastIndex() / colCount);
|
|
}
|
|
}
|
|
public boolean onLongClick(View view)
|
|
{
|
|
if (!enabled)
|
|
return false;
|
|
int index = getLastIndex();
|
|
if (index != -1)
|
|
{
|
|
dragged = index;
|
|
animateDragged();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
@SuppressLint("WrongCall")
|
|
public boolean onTouch(View view, MotionEvent event)
|
|
{
|
|
int action = event.getAction();
|
|
switch (action & MotionEvent.ACTION_MASK) {
|
|
case MotionEvent.ACTION_DOWN:
|
|
enabled = true;
|
|
lastX = (int) event.getX();
|
|
lastY = (int) event.getY();
|
|
touching = true;
|
|
break;
|
|
case MotionEvent.ACTION_MOVE:
|
|
int delta = lastY - (int)event.getY();
|
|
if (dragged != -1)
|
|
{
|
|
//change draw location of dragged visual
|
|
int x = (int)event.getX(), y = (int)event.getY();
|
|
int l = x - (3 * childSize / 4), t = y - (3 * childSize / 4);
|
|
getChildAt(dragged).layout(l, t, l + (childSize * 3 / 2), t + (childSize * 3 / 2));
|
|
|
|
//check for new target hover
|
|
int target = getTargetFromCoor(x, y);
|
|
if (lastTarget != target)
|
|
{
|
|
if (target != -1)
|
|
{
|
|
animateGap(target);
|
|
lastTarget = target;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
scroll += delta;
|
|
clampScroll();
|
|
if (Math.abs(delta) > 2)
|
|
enabled = false;
|
|
onLayout(true, getLeft(), getTop(), getRight(), getBottom());
|
|
}
|
|
lastX = (int) event.getX();
|
|
lastY = (int) event.getY();
|
|
lastDelta = delta;
|
|
break;
|
|
case MotionEvent.ACTION_UP:
|
|
if (dragged != -1)
|
|
{
|
|
View v = getChildAt(dragged);
|
|
if (lastTarget != -1)
|
|
reorderChildren();
|
|
else
|
|
{
|
|
Point xy = getCoorFromIndex(dragged);
|
|
v.layout(xy.x, xy.y, xy.x + childSize, xy.y + childSize);
|
|
}
|
|
v.clearAnimation();
|
|
if (v instanceof ImageView)
|
|
((ImageView)v).setImageAlpha(255);
|
|
lastTarget = -1;
|
|
dragged = -1;
|
|
}
|
|
touching = false;
|
|
break;
|
|
}
|
|
if (dragged != -1)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
//EVENT HELPERS
|
|
protected void animateDragged()
|
|
{
|
|
View v = getChildAt(dragged);
|
|
int x = getCoorFromIndex(dragged).x + childSize / 2, y = getCoorFromIndex(dragged).y + childSize / 2;
|
|
int l = x - (3 * childSize / 4), t = y - (3 * childSize / 4);
|
|
v.layout(l, t, l + (childSize * 3 / 2), t + (childSize * 3 / 2));
|
|
AnimationSet animSet = new AnimationSet(true);
|
|
ScaleAnimation scale = new ScaleAnimation(.667f, 1, .667f, 1, childSize * 3 / 4, childSize * 3 / 4);
|
|
scale.setDuration(animT);
|
|
AlphaAnimation alpha = new AlphaAnimation(1, .5f);
|
|
alpha.setDuration(animT);
|
|
|
|
animSet.addAnimation(scale);
|
|
animSet.addAnimation(alpha);
|
|
animSet.setFillEnabled(true);
|
|
animSet.setFillAfter(true);
|
|
|
|
v.clearAnimation();
|
|
v.startAnimation(animSet);
|
|
}
|
|
protected void animateGap(int target)
|
|
{
|
|
for (int i = 0; i < getChildCount(); i++)
|
|
{
|
|
View v = getChildAt(i);
|
|
if (i == dragged)
|
|
continue;
|
|
int newPos = i;
|
|
if (dragged < target && i >= dragged + 1 && i <= target)
|
|
newPos--;
|
|
else if (target < dragged && i >= target && i < dragged)
|
|
newPos++;
|
|
|
|
//animate
|
|
int oldPos = i;
|
|
if (newPositions.get(i) != -1)
|
|
oldPos = newPositions.get(i);
|
|
if (oldPos == newPos)
|
|
continue;
|
|
|
|
Point oldXY = getCoorFromIndex(oldPos);
|
|
Point newXY = getCoorFromIndex(newPos);
|
|
Point oldOffset = new Point(oldXY.x - v.getLeft(), oldXY.y - v.getTop());
|
|
Point newOffset = new Point(newXY.x - v.getLeft(), newXY.y - v.getTop());
|
|
|
|
TranslateAnimation translate = new TranslateAnimation(Animation.ABSOLUTE, oldOffset.x,
|
|
Animation.ABSOLUTE, newOffset.x,
|
|
Animation.ABSOLUTE, oldOffset.y,
|
|
Animation.ABSOLUTE, newOffset.y);
|
|
translate.setDuration(animT);
|
|
translate.setFillEnabled(true);
|
|
translate.setFillAfter(true);
|
|
v.clearAnimation();
|
|
v.startAnimation(translate);
|
|
|
|
newPositions.set(i, newPos);
|
|
}
|
|
}
|
|
@SuppressLint("WrongCall")
|
|
protected void reorderChildren()
|
|
{
|
|
//FIGURE OUT HOW TO REORDER CHILDREN WITHOUT REMOVING THEM ALL AND RECONSTRUCTING THE LIST!!!
|
|
if (onRearrangeListener != null)
|
|
onRearrangeListener.onRearrange(dragged, lastTarget);
|
|
ArrayList<View> children = new ArrayList<View>();
|
|
for (int i = 0; i < getChildCount(); i++)
|
|
{
|
|
getChildAt(i).clearAnimation();
|
|
children.add(getChildAt(i));
|
|
}
|
|
removeAllViews();
|
|
while (dragged != lastTarget)
|
|
if (lastTarget == children.size()) // dragged and dropped to the right of the last element
|
|
{
|
|
children.add(children.remove(dragged));
|
|
dragged = lastTarget;
|
|
}
|
|
else if (dragged < lastTarget) // shift to the right
|
|
{
|
|
Collections.swap(children, dragged, dragged + 1);
|
|
dragged++;
|
|
}
|
|
else if (dragged > lastTarget) // shift to the left
|
|
{
|
|
Collections.swap(children, dragged, dragged - 1);
|
|
dragged--;
|
|
}
|
|
for (int i = 0; i < children.size(); i++)
|
|
{
|
|
newPositions.set(i, -1);
|
|
addView(children.get(i));
|
|
}
|
|
onLayout(true, getLeft(), getTop(), getRight(), getBottom());
|
|
}
|
|
public void scrollToTop()
|
|
{
|
|
scroll = 0;
|
|
}
|
|
public void scrollToBottom()
|
|
{
|
|
scroll = Integer.MAX_VALUE;
|
|
clampScroll();
|
|
}
|
|
protected void clampScroll()
|
|
{
|
|
int stretch = 3, overreach = getHeight() / 2;
|
|
int max = getMaxScroll();
|
|
max = Math.max(max, 0);
|
|
|
|
if (scroll < -overreach)
|
|
{
|
|
scroll = -overreach;
|
|
lastDelta = 0;
|
|
}
|
|
else if (scroll > max + overreach)
|
|
{
|
|
scroll = max + overreach;
|
|
lastDelta = 0;
|
|
}
|
|
else if (scroll < 0)
|
|
{
|
|
if (scroll >= -stretch)
|
|
scroll = 0;
|
|
else if (!touching)
|
|
scroll -= scroll / stretch;
|
|
}
|
|
else if (scroll > max)
|
|
{
|
|
if (scroll <= max + stretch)
|
|
scroll = max;
|
|
else if (!touching)
|
|
scroll += (max - scroll) / stretch;
|
|
}
|
|
}
|
|
protected int getMaxScroll()
|
|
{
|
|
int rowCount = (int)Math.ceil((double)getChildCount()/colCount), max = rowCount * childSize + (rowCount + 1) * padding - getHeight();
|
|
return max;
|
|
}
|
|
public int getLastIndex()
|
|
{
|
|
return getIndexFromCoor(lastX, lastY);
|
|
}
|
|
|
|
//OTHER METHODS
|
|
public void setOnRearrangeListener(OnRearrangeListener l)
|
|
{
|
|
this.onRearrangeListener = l;
|
|
}
|
|
public void setOnItemClickListener(OnItemClickListener l)
|
|
{
|
|
this.onItemClickListener = l;
|
|
}
|
|
|
|
@Override
|
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
final int count = getChildCount();
|
|
for (int i = 0; i < count; i++) {
|
|
getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
|
|
}
|
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
|
|
}
|
|
} |