Share My Creation DragView (donationware)

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 buy the "seats" to enjoy it!
 
Last edited:

jkhazraji

Active Member
Licensed User
Longtime User
It seems that your noise is still going nonstop for 21 years now! God helps listeners
 
Last edited:

Cableguy

Expert
Licensed User
Longtime User
Well, you know, music is a sort of noise, so is sound produced my heavy machinery, or even bees flying...

Still, yes, making noise for over 21 years now, but quality noise, even if it is not for your taste.

And yet, it made you reconsider...
 

aeric

Expert
Licensed User
Longtime User
The OP just need to edit the title by appending [Chargeable].
 
Top