A reliable approach to handling interactive view reordering or selection involves capturing touch movements and rendering a transient visual proxy above the existing UI hierarchy. This technique leverages Android's WindowManager to display a semi-transparent bitmap overlay that tracks the user's finger with out intercepting gestures meant for underlying layout elements. Below is a production-ready implementation that encapsulates the entire lifecycle, from view attachment to spatial collision detection.
Core Controller Implementation
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DragProxyController implements View.OnTouchListener {
private final Context context;
private final WindowManager windowManager;
private final Map<View, RectInfo> targetBounds = new HashMap<>();
private final List<View> draggableTargets = new ArrayList<>();
private ImageView floatProxy;
private Bitmap cachedFrame;
private WindowManager.LayoutParams proxyParams;
private boolean isDragging = false;
private View activeTarget;
private float initialTouchOffsetX, initialTouchOffsetY;
private OnDragInteractionListener interactionCallback;
public DragProxyController(Context appContext, OnDragInteractionListener callback) {
this.context = appContext.getApplicationContext();
this.windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
this.interactionCallback = callback;
}
public void attachTargets(View... views) {
draggableTargets.clear();
targetBounds.clear();
for (View v : views) {
v.setOnTouchListener(this);
draggableTargets.add(v);
}
}
@Override
public boolean onTouch(View view, MotionEvent event) {
int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
captureBounds();
initializeFloatOverlay(view, event);
return true;
case MotionEvent.ACTION_MOVE:
if (isDragging && floatProxy != null) {
updateOverlayPosition(event.getRawX(), event.getRawY());
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
performDropCheck(event);
dismissOverlay();
isDragging = false;
activeTarget = null;
return true;
}
return false;
}
private void captureBounds() {
int[] location = new int[2];
targetBounds.clear();
for (View target : draggableTargets) {
target.getLocationInWindow(location);
targetBounds.put(target, new RectInfo(
location[0],
location[1],
target.getWidth(),
target.getHeight()
));
}
}
private void initializeFloatOverlay(View source, MotionEvent e) {
source.setDrawingCacheEnabled(true);
source.buildDrawingCache(true);
cachedFrame = Bitmap.createBitmap(source.getDrawingCache());
source.setDrawingCacheEnabled(false);
source.destroyDrawingCache();
if (cachedFrame == null) return;
proxyParams = new WindowManager.LayoutParams();
proxyParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
proxyParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
proxyParams.gravity = Gravity.TOP | Gravity.START;
proxyParams.format = PixelFormat.TRANSLUCENT;
proxyParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
proxyParams.alpha = 0.75f;
proxyParams.x = (int) e.getRawX();
proxyParams.y = (int) e.getRawY();
floatProxy = new ImageView(context);
floatProxy.setImageBitmap(cachedFrame);
windowManager.addView(floatProxy, proxyParams);
isDragging = true;
activeTarget = source;
RectInfo info = targetBounds.get(source);
if (info != null) {
initialTouchOffsetX = e.getRawX() - info.left;
initialTouchOffsetY = e.getRawY() - info.top;
}
if (interactionCallback != null) {
interactionCallback.onDragStarted(source.getId());
}
}
private void updateOverlayPosition(float rawX, float rawY) {
proxyParams.x = (int) (rawX - initialTouchOffsetX);
proxyParams.y = (int) (rawY - initialTouchOffsetY);
windowManager.updateViewLayout(floatProxy, proxyParams);
}
private void performDropCheck(MotionEvent e) {
if (activeTarget == null || targetBounds.isEmpty()) return;
float touchX = e.getRawX();
float touchY = e.getRawY();
int overlappingId = -1;
for (Map.Entry<View, RectInfo> entry : targetBounds.entrySet()) {
View target = entry.getKey();
RectInfo bounds = entry.getValue();
if (target.getId() != activeTarget.getId() &&
touchX >= bounds.left && touchX <= bounds.right &&
touchY >= bounds.top && touchY <= bounds.bottom) {
overlappingId = target.getId();
break;
}
}
if (overlappingId != -1 && interactionCallback != null) {
interactionCallback.onDragDropped(activeTarget.getId(), overlappingId);
}
}
private void dismissOverlay() {
if (floatProxy != null) {
windowManager.removeViewImmediate(floatProxy);
floatProxy.setImageDrawable(null);
floatProxy = null;
}
if (cachedFrame != null && !cachedFrame.isRecycled()) {
cachedFrame.recycle();
cachedFrame = null;
}
}
public void release() {
dismissOverlay();
draggableTargets.clear();
targetBounds.clear();
}
public interface OnDragInteractionListener {
void onDragStarted(int viewId);
void onDragDropped(int startViewId, int targetViewId);
}
private static class RectInfo {
final int left, top, right, bottom;
RectInfo(int l, int t, int w, int h) {
this.left = l;
this.top = t;
this.right = l + w;
this.bottom = t + h;
}
}
}
Integration and Event Handlign
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
public class DragDemoActivity extends AppCompatActivity {
private DragProxyController dragHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo);
TextView labelPrimary = findViewById(R.id.tv_header);
TextView labelSecondary = findViewById(R.id.tv_subhead);
Button actionButton = findViewById(R.id.btn_trigger);
dragHandler = new DragProxyController(this, new DragProxyController.OnDragInteractionListener() {
@Override
public void onDragStarted(int viewId) {
Toast.makeText(DragDemoActivity.this, "Grab initiated", Toast.LENGTH_SHORT).show();
}
@Override
public void onDragDropped(int originId, int destId) {
Toast.makeText(DragDemoActivity.this, "Released on component: " + destId, Toast.LENGTH_SHORT).show();
}
});
dragHandler.attachTargets(labelPrimary, labelSecondary, actionButton);
labelPrimary.setOnClickListener(v -> showToast("Primary clicked"));
actionButton.setOnClickListener(v -> showToast("Action button tapped"));
}
private void showToast(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (dragHandler != null) {
dragHandler.release();
}
}
}
The controller operates by registering a single OnTouchListener across all attached components. Upon receiving ACTION_DOWN, it snapshots the current screen coordinates of every bound element and generates a translucent bitmap representation. During ACTION_MOVE, the overlay position is recalculated using raw pointer coordinates minus the initial tap offset, ensuring the visual proxy remains aligned with the user's grip point rather than snapping to the top-left corner. When the gesture concludes, an axis-aligned bounding box (AABB) comparison determines weather the drop landed within the perimeter of another registered view. Resources such as the overlay window and temporary bitmap are explicitly freed via release() to prevent memory leaks during activity rotation or configuration changes.