Share My Creation DragView

DragLayoutView
A view that allows dragging child views manually.
Method: AddDraggableItem
Event : swipe
dragview.gif


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:

Cableguy

Expert
Licensed User
Longtime User
This is click bait, ransom coding!
Why not specify clearly in the description that the lib/code is donationware.??
Instead, we "discover" this info in a line of code...

Not the best way to do so!
It's like offering a free meal only to discover you must by the "seats" to enjoy it!
 
Top