DragLayoutView
A view that allows dragging child views manually.
Method: AddDraggableItem
Event : swipe
A view that allows dragging child views manually.
Method: AddDraggableItem
Event : swipe
B4X:
#Region Project Attributes
#ApplicationLabel: B4A Example
#VersionCode: 1
#VersionName:
'SupportedOrientations possible values: unspecified, landscape or portrait.
#SupportedOrientations: unspecified
#CanInstallToExternalStorage: False
#AdditionalJar: core-lambda-stubs.jar, ReferenceOnly
#End Region
#Region Activity Attributes
#FullScreen: False
#IncludeTitle: True
#End Region
Sub Process_Globals
Private xui As XUI
Private dadview As JavaObject
Private ItemViews As List
End Sub
Sub Globals
End Sub
Sub Activity_Create(FirstTime As Boolean)
Activity.LoadLayout("Layout")
Activity.Color=Colors.DarkGray
Dim context As JavaObject
context.InitializeContext
Dim LP As JavaObject
LP.InitializeNewInstance("android.view.ViewGroup$LayoutParams", Array(100%x, 100%y))
dadview.InitializeNewInstance(Application.PackageName & ".main$DragLinearLayout",Array(context))
dadview.As(JavaObject).RunMethod("setLayoutParams", Array(LP))
Activity.AddView(dadview,0,0,100%x,100%y)
' Add Items
CreateAndAddItem("Item 1", Colors.Red)
CreateAndAddItem("Item 2", Colors.Blue)
CreateAndAddItem("Item 3", Colors.Green)
createEvent
End Sub
Sub createEvent
'''
Dim event As Object = dadview.As(JavaObject).CreateEventFromUI(Application.PackageName & ".main$DragLinearLayout.OnViewSwapListener","swipe",Null) 'See: swipe_Event
dadview.As(JavaObject).RunMethod("setOnViewSwapListener",Array(event))
'''
End Sub
Sub swipe_Event(methodName As String, Args() As Object)
'Method Name: onSwipe
Log(methodName)
Log(Args(0))
Log(Args(1))
Log(Args(2))
Log(Args(3))
End Sub
Sub CreateAndAddItem(Text As String, Color As Int)
Dim pItem As Panel
pItem.Initialize("")
pItem.Color = Color
pItem.Height = 100dip
Dim lbl As Label
lbl.Initialize("")
lbl.Text = Text
lbl.TextColor = Colors.White
lbl.Gravity = Gravity.CENTER
pItem.AddView(lbl, 0, 0, 100%x, 100dip)
Dim btnHandle As Button
btnHandle.Initialize("")
btnHandle.Text = ":::"
pItem.AddView(btnHandle, 10dip, 10dip, 50dip, 50dip)
AddDraggableItem(dadview, Text, Color)
End Sub
Sub AddDraggableItem(ParentJO As JavaObject, Text As String, Color As Int)
Dim pItem As Panel
pItem.Initialize("")
pItem.Color = Color
Dim lbl As Label
lbl.Initialize("")
lbl.Text = Text
lbl.TextColor = Colors.White
lbl.Gravity = Gravity.CENTER
pItem.AddView(lbl, 0, 0, 100%x, 100dip) ' Label fills panel
Dim JO_LayoutParams As JavaObject
JO_LayoutParams.InitializeNewInstance("android.widget.LinearLayout$LayoutParams", Array(100%x, 100dip))
ParentJO.RunMethod("addView", Array(pItem, JO_LayoutParams))
ParentJO.RunMethod("setViewDraggable", Array(pItem, pItem))
End Sub
#if java
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.LayoutTransition;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewTreeObserver;
import android.widget.LinearLayout;
import android.widget.ScrollView;
/**
* A LinearLayout that supports children Views that can be dragged and swapped around.
*/
public static class DragLinearLayout extends LinearLayout {
private static final String LOG_TAG = DragLinearLayout.class.getSimpleName();
private static final long NOMINAL_SWITCH_DURATION = 150;
private static final long MIN_SWITCH_DURATION = NOMINAL_SWITCH_DURATION;
private static final long MAX_SWITCH_DURATION = NOMINAL_SWITCH_DURATION * 2;
private static final float NOMINAL_DISTANCE = 20;
private final float nominalDistanceScaled;
public interface OnViewSwapListener {
void onSwap(View firstView, int firstPosition, View secondView, int secondPosition);
}
private OnViewSwapListener swapListener;
private LayoutTransition layoutTransition;
private final SparseArray<DraggableChild> draggableChildren;
private class DraggableChild {
private ValueAnimator swapAnimation;
public void endExistingAnimation() {
if (swapAnimation != null) swapAnimation.end();
}
public void cancelExistingAnimation() {
if (swapAnimation != null) swapAnimation.cancel();
}
}
private class DragItem {
private View view;
private int startVisibility;
private BitmapDrawable viewDrawable;
private int position;
private int startTop;
private int height;
private int totalDragOffset;
private int targetTopOffset;
private ValueAnimator settleAnimation;
private boolean detecting;
private boolean dragging;
public DragItem() {
stopDetecting();
}
public void startDetectingOnPossibleDrag(final View view, final int position) {
this.view = view;
this.startVisibility = view.getVisibility();
this.viewDrawable = getDragDrawable(view);
this.position = position;
this.startTop = view.getTop();
this.height = view.getHeight();
this.totalDragOffset = 0;
this.targetTopOffset = 0;
this.settleAnimation = null;
this.detecting = true;
}
public void onDragStart() {
if (view != null) view.setVisibility(View.INVISIBLE);
this.dragging = true;
}
public void setTotalOffset(int offset) {
totalDragOffset = offset;
updateTargetTop();
}
public void updateTargetTop() {
if (view != null) {
targetTopOffset = startTop - view.getTop() + totalDragOffset;
}
}
public void onDragStop() {
this.dragging = false;
}
public boolean settling() {
return settleAnimation != null;
}
public void stopDetecting() {
this.detecting = false;
if (view != null) view.setVisibility(startVisibility);
view = null;
startVisibility = -1;
viewDrawable = null;
position = -1;
startTop = -1;
height = -1;
totalDragOffset = 0;
targetTopOffset = 0;
if (settleAnimation != null) settleAnimation.end();
settleAnimation = null;
}
}
private final DragItem draggedItem;
private final int slop;
private static final int INVALID_POINTER_ID = -1;
private int downY = -1;
private int activePointerId = INVALID_POINTER_ID;
private final Drawable dragTopShadowDrawable;
private final Drawable dragBottomShadowDrawable;
private final int dragShadowHeight;
private ScrollView containerScrollView;
private int scrollSensitiveAreaHeight;
private static final int DEFAULT_SCROLL_SENSITIVE_AREA_HEIGHT_DP = 48;
private static final int MAX_DRAG_SCROLL_SPEED = 16;
public DragLinearLayout(Context context) {
this(context, null);
}
public DragLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setOrientation(LinearLayout.VERTICAL);
draggableChildren = new SparseArray<>();
draggedItem = new DragItem();
ViewConfiguration vc = ViewConfiguration.get(context);
slop = vc.getScaledTouchSlop();
final Resources resources = getResources();
// Create programmatic shadows instead of relying on missing XML resources
dragTopShadowDrawable = createShadowDrawable(true);
dragBottomShadowDrawable = createShadowDrawable(false);
dragShadowHeight = (int) (10 * resources.getDisplayMetrics().density + 0.5f); // Default 10dp
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DragLinearLayout, 0, 0);
try {
scrollSensitiveAreaHeight = a.getDimensionPixelSize(R.styleable.DragLinearLayout_scrollSensitiveHeight,
(int) (DEFAULT_SCROLL_SENSITIVE_AREA_HEIGHT_DP * resources.getDisplayMetrics().density + 0.5f));
} finally {
a.recycle();
}
nominalDistanceScaled = (int) (NOMINAL_DISTANCE * resources.getDisplayMetrics().density + 0.5f);
}
private Drawable createShadowDrawable(boolean isTop) {
GradientDrawable gd = new GradientDrawable();
// Simple gray shadow
int color = 0x44000000;
gd.setColor(color);
return gd;
}
@Override
public void setOrientation(int orientation) {
if (LinearLayout.HORIZONTAL == orientation) {
throw new IllegalArgumentException("DragLinearLayout must be VERTICAL.");
}
super.setOrientation(orientation);
}
public void addDragView(View child, View dragHandle) {
addView(child);
setViewDraggable(child, dragHandle);
}
public void addDragView(View child, View dragHandle, int index) {
addView(child, index);
final int numMappings = draggableChildren.size();
for (int i = numMappings - 1; i >= 0; i--) {
final int key = draggableChildren.keyAt(i);
if (key >= index) {
draggableChildren.put(key + 1, draggableChildren.get(key));
}
}
setViewDraggable(child, dragHandle);
}
public void setViewDraggable(View child, View dragHandle) {
if (child == null || dragHandle == null) {
throw new IllegalArgumentException("Draggable children and their drag handles must not be null.");
}
if (this == child.getParent()) {
dragHandle.setOnTouchListener(new DragHandleOnTouchListener(child));
draggableChildren.put(indexOfChild(child), new DraggableChild());
} else {
Log.e(LOG_TAG, child + " is not a child, cannot make draggable.");
}
}
public void removeDragView(View child) {
if (this == child.getParent()) {
final int index = indexOfChild(child);
removeView(child);
final int mappings = draggableChildren.size();
for (int i = 0; i < mappings; i++) {
final int key = draggableChildren.keyAt(i);
if (key >= index) {
DraggableChild next = draggableChildren.get(key + 1);
if (next == null) {
draggableChildren.delete(key);
} else {
draggableChildren.put(key, next);
}
}
}
}
}
@Override
public void removeAllViews() {
super.removeAllViews();
draggableChildren.clear();
}
public void setContainerScrollView(ScrollView scrollView) {
this.containerScrollView = scrollView;
}
public void setScrollSensitiveHeight(int height) {
this.scrollSensitiveAreaHeight = height;
}
public int getScrollSensitiveHeight() {
return scrollSensitiveAreaHeight;
}
public void setOnViewSwapListener(OnViewSwapListener swapListener) {
this.swapListener = swapListener;
}
private long getTranslateAnimationDuration(float distance) {
return Math.min(MAX_SWITCH_DURATION, Math.max(MIN_SWITCH_DURATION,
(long) (NOMINAL_SWITCH_DURATION * Math.abs(distance) / nominalDistanceScaled)));
}
private void startDetectingDrag(View child) {
if (draggedItem.detecting) return;
final int position = indexOfChild(child);
// Safety check
if (position < 0 || position >= draggableChildren.size()) return;
draggableChildren.get(position).endExistingAnimation();
draggedItem.startDetectingOnPossibleDrag(child, position);
if (containerScrollView != null) {
containerScrollView.requestDisallowInterceptTouchEvent(true);
}
}
private void startDrag() {
layoutTransition = getLayoutTransition();
if (layoutTransition != null) {
setLayoutTransition(null);
}
draggedItem.onDragStart();
requestDisallowInterceptTouchEvent(true);
}
private void onDragStop() {
if (draggedItem.view == null) return;
draggedItem.settleAnimation = ValueAnimator.ofFloat(draggedItem.totalDragOffset,
draggedItem.totalDragOffset - draggedItem.targetTopOffset)
.setDuration(getTranslateAnimationDuration(draggedItem.targetTopOffset));
// REPLACED LAMBDA WITH ANONYMOUS CLASS
draggedItem.settleAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (!draggedItem.detecting) return;
draggedItem.setTotalOffset(((Float) animation.getAnimatedValue()).intValue());
final int shadowAlpha = (int) ((1 - animation.getAnimatedFraction()) * 255);
if (dragTopShadowDrawable != null) dragTopShadowDrawable.setAlpha(shadowAlpha);
if (dragBottomShadowDrawable != null) dragBottomShadowDrawable.setAlpha(shadowAlpha);
invalidate();
}
});
draggedItem.settleAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
draggedItem.onDragStop();
}
@Override
public void onAnimationEnd(Animator animation) {
if (!draggedItem.detecting) return;
draggedItem.settleAnimation = null;
draggedItem.stopDetecting();
if (dragTopShadowDrawable != null) dragTopShadowDrawable.setAlpha(255);
if (dragBottomShadowDrawable != null) dragBottomShadowDrawable.setAlpha(255);
if (layoutTransition != null && getLayoutTransition() == null) {
setLayoutTransition(layoutTransition);
}
}
});
draggedItem.settleAnimation.start();
}
private void onDrag(final int offset) {
draggedItem.setTotalOffset(offset);
invalidate();
if (draggedItem.view == null) return;
int currentTop = draggedItem.startTop + draggedItem.totalDragOffset;
handleContainerScroll(currentTop);
int belowPosition = nextDraggablePosition(draggedItem.position);
int abovePosition = previousDraggablePosition(draggedItem.position);
View belowView = getChildAt(belowPosition);
View aboveView = getChildAt(abovePosition);
final boolean isBelow = (belowView != null) &&
(currentTop + draggedItem.height > belowView.getTop() + belowView.getHeight() / 2);
final boolean isAbove = (aboveView != null) &&
(currentTop < aboveView.getTop() + aboveView.getHeight() / 2);
if (isBelow || isAbove) {
final View switchView = isBelow ? belowView : aboveView;
final int originalPosition = draggedItem.position;
final int switchPosition = isBelow ? belowPosition : abovePosition;
if (switchPosition < 0 || switchPosition >= draggableChildren.size()) return;
draggableChildren.get(switchPosition).cancelExistingAnimation();
final float switchViewStartY = switchView.getY();
if (swapListener != null) {
swapListener.onSwap(draggedItem.view, draggedItem.position, switchView, switchPosition);
}
if (isBelow) {
removeViewAt(originalPosition);
removeViewAt(switchPosition - 1);
addView(belowView, originalPosition);
addView(draggedItem.view, switchPosition);
} else {
removeViewAt(switchPosition);
removeViewAt(originalPosition - 1);
addView(draggedItem.view, switchPosition);
addView(aboveView, originalPosition);
}
draggedItem.position = switchPosition;
final ViewTreeObserver switchViewObserver = switchView.getViewTreeObserver();
switchViewObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
switchViewObserver.removeOnPreDrawListener(this);
final ObjectAnimator switchAnimator = ObjectAnimator.ofFloat(switchView, "y",
switchViewStartY, switchView.getTop())
.setDuration(getTranslateAnimationDuration(switchView.getTop() - switchViewStartY));
switchAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
if (originalPosition < draggableChildren.size()) {
draggableChildren.get(originalPosition).swapAnimation = switchAnimator;
}
}
@Override
public void onAnimationEnd(Animator animation) {
if (originalPosition < draggableChildren.size()) {
draggableChildren.get(originalPosition).swapAnimation = null;
}
}
});
switchAnimator.start();
return true;
}
});
final ViewTreeObserver observer = draggedItem.view.getViewTreeObserver();
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
observer.removeOnPreDrawListener(this);
draggedItem.updateTargetTop();
if (draggedItem.settling()) {
Log.d(LOG_TAG, "Updating settle animation");
draggedItem.settleAnimation.removeAllListeners();
draggedItem.settleAnimation.cancel();
onDragStop();
}
return true;
}
});
}
}
private int previousDraggablePosition(int position) {
int startIndex = draggableChildren.indexOfKey(position);
if (startIndex < 1 || startIndex > draggableChildren.size()) return -1;
return draggableChildren.keyAt(startIndex - 1);
}
private int nextDraggablePosition(int position) {
int startIndex = draggableChildren.indexOfKey(position);
if (startIndex < -1 || startIndex > draggableChildren.size() - 2) return -1;
return draggableChildren.keyAt(startIndex + 1);
}
private Runnable dragUpdater;
private void handleContainerScroll(final int currentTop) {
if (containerScrollView != null) {
final int startScrollY = containerScrollView.getScrollY();
final int absTop = getTop() - startScrollY + currentTop;
final int height = containerScrollView.getHeight();
final int delta;
if (absTop < scrollSensitiveAreaHeight) {
delta = (int) (-MAX_DRAG_SCROLL_SPEED * smootherStep(scrollSensitiveAreaHeight, 0, absTop));
} else if (absTop > height - scrollSensitiveAreaHeight) {
delta = (int) (MAX_DRAG_SCROLL_SPEED * smootherStep(height - scrollSensitiveAreaHeight, height, absTop));
} else {
delta = 0;
}
containerScrollView.removeCallbacks(dragUpdater);
containerScrollView.smoothScrollBy(0, delta);
dragUpdater = () -> {
if (draggedItem.dragging && startScrollY != containerScrollView.getScrollY()) {
onDrag(draggedItem.totalDragOffset + delta);
}
};
containerScrollView.post(dragUpdater);
}
}
private static float smootherStep(float edge1, float edge2, float val) {
val = Math.max(0, Math.min((val - edge1) / (edge2 - edge1), 1));
return val * val * val * (val * (val * 6 - 15) + 10);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (draggedItem.detecting && (draggedItem.dragging || draggedItem.settling())) {
if (draggedItem.viewDrawable == null) return;
canvas.save();
canvas.translate(0, draggedItem.totalDragOffset);
draggedItem.viewDrawable.draw(canvas);
final Rect bounds = draggedItem.viewDrawable.getBounds();
final int left = bounds.left;
final int right = bounds.right;
final int top = bounds.top;
final int bottom = bounds.bottom;
if (dragBottomShadowDrawable != null) {
dragBottomShadowDrawable.setBounds(left, bottom, right, bottom + dragShadowHeight);
dragBottomShadowDrawable.draw(canvas);
}
if (dragTopShadowDrawable != null) {
dragTopShadowDrawable.setBounds(left, top - dragShadowHeight, right, top);
dragTopShadowDrawable.draw(canvas);
}
canvas.restore();
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
if (draggedItem.detecting) return false;
downY = (int) event.getY(0);
activePointerId = event.getPointerId(0);
break;
}
case MotionEvent.ACTION_MOVE: {
if (!draggedItem.detecting) return false;
if (INVALID_POINTER_ID == activePointerId) break;
final int pointerIndex = event.findPointerIndex(activePointerId);
if (pointerIndex < 0) break;
final float y = event.getY(pointerIndex);
final float dy = y - downY;
if (Math.abs(dy) > slop) {
startDrag();
return true;
}
return false;
}
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = event.getActionIndex();
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId != activePointerId) break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
onTouchEnd();
if (draggedItem.detecting) draggedItem.stopDetecting();
break;
}
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
if (!draggedItem.detecting || draggedItem.settling()) return false;
startDrag();
return true;
}
case MotionEvent.ACTION_MOVE: {
if (!draggedItem.dragging) break;
if (INVALID_POINTER_ID == activePointerId) break;
int pointerIndex = event.findPointerIndex(activePointerId);
if (pointerIndex < 0) break;
int lastEventY = (int) event.getY(pointerIndex);
int deltaY = lastEventY - downY;
onDrag(deltaY);
return true;
}
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = event.getActionIndex();
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId != activePointerId) break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
onTouchEnd();
if (draggedItem.dragging) {
onDragStop();
} else if (draggedItem.detecting) {
draggedItem.stopDetecting();
}
return true;
}
}
return false;
}
private void onTouchEnd() {
downY = -1;
activePointerId = INVALID_POINTER_ID;
}
private class DragHandleOnTouchListener implements OnTouchListener {
private final View view;
public DragHandleOnTouchListener(final View view) {
this.view = view;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
startDetectingDrag(view);
}
return false;
}
}
private BitmapDrawable getDragDrawable(View view) {
int top = view.getTop();
int left = view.getLeft();
Bitmap bitmap = getBitmapFromView(view);
BitmapDrawable drawable = new BitmapDrawable(getResources(), bitmap);
drawable.setBounds(new Rect(left, top, left + view.getWidth(), top + view.getHeight()));
return drawable;
}
private static Bitmap getBitmapFromView(View view) {
Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
view.draw(canvas);
return bitmap;
}
}
#end if
Last edited: