/* * Catroid: An on-device visual programming system for Android devices * Copyright (C) 2010-2016 The Catrobat Team * (<http://developer.catrobat.org/credits>) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * An additional term exception under section 7 of the GNU Affero * General Public License, version 3, is available at * http://developer.catrobat.org/license_additional_term * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /** * Author: Romain Guy * <p/> * Using example: <?xml version="4.0" encoding="utf-8"?> <com.example.android.layout.FlowLayout * xmlns:f="http://schemas.android.com/apk/res/org.apmem.android" * xmlns:android="http://schemas.android.com/apk/res/android" f:horizontalSpacing="6dip" f:verticalSpacing="12dip" * android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingLeft="6dip" * android:paddingTop="6dip" android:paddingRight="12dip"> <Button android:layout_width="wrap_content" * android:layout_height="wrap_content" f:layout_horizontalSpacing="32dip" f:layout_breakLine="true" * android:text="Cancel" /> * <p/> * </com.example.android.layout.FlowLayout> */ package org.catrobat.catroid.ui; import android.content.Context; import android.graphics.Bitmap; import android.graphics.PixelFormat; import android.util.AttributeSet; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.widget.ImageView; import android.widget.Spinner; import org.catrobat.catroid.R; import org.catrobat.catroid.common.ScreenValues; import java.util.LinkedList; public class DragNDropBrickLayout extends BrickLayout { private boolean dragging; private static final float BIAS_SAME_LINE_FUDGE_FACTOR = 10; private static final int MIN_MILLISECONDS_FOR_TAP = 300; private int lastInsertableSpaceIndex; private boolean justStartedDragging; private boolean secondDragFrame; private int draggedItemIndex; private int dragPointOffsetX; private int dragPointOffsetY; private int viewToWindowSpaceX; private int viewToWindowSpaceY; private long dragBeganMillis; private long dragEndMillis; private View draggedItemInLayout; private WeirdFloatingWindowData dragView; private WeirdFloatingWindowData dragCursor1; private WeirdFloatingWindowData dragCursor2; private LineBreakListener lineBreakListener; private LinkedList<Integer> breaks; public DragAndDropBrickLayoutListener parent; public DragNDropBrickLayout(Context context) { super(context); } public DragNDropBrickLayout(Context context, AttributeSet attributeSet) { super(context, attributeSet); } public DragNDropBrickLayout(Context context, AttributeSet attributeSet, int defStyle) { super(context, attributeSet, defStyle); } public void setListener(DragAndDropBrickLayoutListener parent) { this.parent = parent; } public void registerLineBreakListener(LineBreakListener listener) { lineBreakListener = listener; } @Override protected void allocateLineData() { breaks = new LinkedList<>(); super.allocateLineData(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - this.getPaddingRight() - this.getPaddingLeft(); int sizeHeight = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom(); int modeWidth = MeasureSpec.getMode(widthMeasureSpec); int modeHeight = MeasureSpec.getMode(heightMeasureSpec); int lineThicknessWithHorizontalSpacing; int lineThickness; int lineLengthWithHorizontalSpacing; int lineLength; int prevLinePosition; int controlMaxLength; int controlMaxThickness; for (LineData lineData : lines) { lineData.allowableTextFieldWidth = 0; lineData.height = 0; lineData.minHeight = 0; lineData.numberOfTextFields = 0; lineData.totalTextFieldWidth = 0; for (ElementData elementData : lineData.elements) { elementData.height = 0; elementData.width = 0; elementData.posY = 0; elementData.posX = 0; elementData.view = null; } } LineData currentLine; lineThicknessWithHorizontalSpacing = 0; lineThickness = 0; lineLengthWithHorizontalSpacing = 0; prevLinePosition = 0; controlMaxLength = 0; controlMaxThickness = 0; currentLine = lines.getFirst(); int elementInLineIndex = 0; for (int i = 0; i < getChildCount(); i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } boolean forceNewLine = false; LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); int horizontalSpacing = getHorizontalSpacing(layoutParams); int verticalSpacing = getVerticalSpacing(layoutParams); if (child instanceof Spinner) { child.measure(MeasureSpec.makeMeasureSpec(sizeWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( sizeHeight, modeHeight == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeHeight)); } else if (layoutParams.getNewLine()) { int width = sizeWidth - (lineLengthWithHorizontalSpacing + horizontalSpacing); if (width <= horizontalSpacing * 2) { forceNewLine = true; width = sizeWidth - (horizontalSpacing * 4); } child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( sizeHeight, modeHeight == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeHeight)); } else { child.measure(MeasureSpec.makeMeasureSpec(sizeWidth, modeWidth == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeWidth), MeasureSpec .makeMeasureSpec(sizeHeight, modeHeight == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeHeight)); } int childWidth = child.getMeasuredWidth(); int childHeight = child.getMeasuredHeight(); boolean updateSmallestHeight = currentLine.minHeight == 0 || currentLine.minHeight > childHeight; currentLine.minHeight = (updateSmallestHeight ? childHeight : currentLine.minHeight); lineLength = lineLengthWithHorizontalSpacing + childWidth; lineLengthWithHorizontalSpacing = lineLength + horizontalSpacing; boolean previousWasNewLine = false; if (i > 0) { LayoutParams previousLayoutParams = (LayoutParams) getChildAt(i - 1).getLayoutParams(); previousWasNewLine = previousLayoutParams.getNewLine(); } if (lineLength > sizeWidth || previousWasNewLine || forceNewLine) { prevLinePosition = prevLinePosition + lineThicknessWithHorizontalSpacing; currentLine = getNextLine(currentLine); elementInLineIndex = 0; lineThickness = childHeight; lineLength = childWidth; lineThicknessWithHorizontalSpacing = childHeight + verticalSpacing; lineLengthWithHorizontalSpacing = lineLength + horizontalSpacing; } lineThicknessWithHorizontalSpacing = Math.max(lineThicknessWithHorizontalSpacing, childHeight + verticalSpacing); lineThickness = Math.max(lineThickness, childHeight); currentLine.height = lineThickness; int posX = getPaddingLeft() + lineLength - childWidth; int posY = getPaddingTop() + prevLinePosition; ElementData element = getElement(currentLine, elementInLineIndex); element.view = child; element.posX = posX; element.posY = posY; element.width = childWidth; element.height = childHeight; elementInLineIndex++; controlMaxLength = Math.max(controlMaxLength, lineLength); controlMaxThickness = prevLinePosition + lineThickness; } int x = controlMaxLength; int y = controlMaxThickness; y += getPaddingTop() + getPaddingBottom(); int centerVertically = 0; if (y < getSuggestedMinimumHeight()) { centerVertically = (getSuggestedMinimumHeight() - y) / 2; } y = Math.max(y, getSuggestedMinimumHeight()); int i = 0; breaks.clear(); for (LineData lineData : lines) { boolean firstInLine = true; for (ElementData elementData : lineData.elements) { if (elementData.view != null) { if (firstInLine && i != 0) { breaks.add(i); } firstInLine = false; int centerVerticallyWithinLine = 0; if (elementData.height < lineData.height) { centerVerticallyWithinLine = Math.round((lineData.height - elementData.height) * 0.5f); } elementData.posY += centerVertically + centerVerticallyWithinLine; LayoutParams layoutParams = (LayoutParams) elementData.view.getLayoutParams(); layoutParams.setPosition(elementData.posX, elementData.posY); i++; } } } if (lineBreakListener != null) { lineBreakListener.setBreaks(breaks); } this.setMeasuredDimension(resolveSize(x, widthMeasureSpec), resolveSize(y, heightMeasureSpec)); } @Override public boolean onTouchEvent(MotionEvent ev) { final int action = ev.getAction(); final int x = (int) ev.getX(); final int y = (int) ev.getY(); viewToWindowSpaceX = (int) ev.getRawX() - x; viewToWindowSpaceY = (int) ev.getRawY() - y; switch (action) { case MotionEvent.ACTION_DOWN: int itemPosition = click(x, y); if (itemPosition != -1) { beginDrag(x, y, itemPosition); } break; case MotionEvent.ACTION_MOVE: if (dragging) { drag(x, y); } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: default: if (dragging) { drop(); } break; } return true; } private int click(int x, int y) { int itemPosition = 0; for (BrickLayout.LineData line : lines) { for (BrickLayout.ElementData e : line.elements) { if (e.view != null) { if (x > e.posX && y > e.posY && x < e.posX + e.width && y < e.posY + e.height) { dragPointOffsetX = (e.posX - x); dragPointOffsetY = (e.posY - y); return itemPosition; } itemPosition++; } } } return -1; } private void beginDrag(int x, int y, int itemIndex) { dragBeganMillis = System.currentTimeMillis(); // frequent dragdrops can cause a null reference when the event for the new drag happens before the drop finishes. if (dragging || dragBeganMillis - dragEndMillis < 200) { return; } justStartedDragging = true; draggedItemIndex = itemIndex; stopDrag(); draggedItemInLayout = getChildAt(itemIndex); if (draggedItemInLayout == null) { return; } draggedItemInLayout.setDrawingCacheEnabled(true); // Create a copy of the drawing cache so that it does not get recycled // by the framework when the list tries to clean up memory Bitmap bitmap = Bitmap.createBitmap(draggedItemInLayout.getDrawingCache()); dragView = makeWeirdFloatingWindow(bitmap, draggedItemInLayout.getWidth(), draggedItemInLayout.getHeight()); dragCursor1 = makeWeirdFloatingWindow(View.inflate(getContext(), R.layout.brick_user_data_insert, null)); dragCursor2 = makeWeirdFloatingWindow(View.inflate(getContext(), R.layout.brick_user_data_insert, null)); dragging = true; drag(x, y); } // move the drag view private void drag(int x, int y) { int centerOfDraggedElementX = x + dragPointOffsetX; int centerOfDraggedElementY = y + dragPointOffsetY; positionWierdFloatingWindow(dragView, centerOfDraggedElementX, centerOfDraggedElementY); int insertableSpaceIndex = findClosestInsertableSpace(centerOfDraggedElementX, centerOfDraggedElementY); if (secondDragFrame) { draggedItemInLayout.setVisibility(View.INVISIBLE); secondDragFrame = false; } if (justStartedDragging || lastInsertableSpaceIndex != insertableSpaceIndex) { repositionCursors(insertableSpaceIndex); lastInsertableSpaceIndex = insertableSpaceIndex; justStartedDragging = false; secondDragFrame = true; } } private void drop() { dragEndMillis = System.currentTimeMillis(); long difference = dragEndMillis - dragBeganMillis; if (difference < MIN_MILLISECONDS_FOR_TAP && (draggedItemIndex == lastInsertableSpaceIndex || draggedItemIndex == lastInsertableSpaceIndex + 1)) { parent.click(draggedItemIndex); } else { parent.reorder(draggedItemIndex, lastInsertableSpaceIndex); } stopDrag(); } private void stopDrag() { removeWeirdFloatingWindow(dragView); removeWeirdFloatingWindow(dragCursor1); removeWeirdFloatingWindow(dragCursor2); dragView = null; dragCursor1 = null; dragCursor2 = null; View item = getChildAt(draggedItemIndex); if (item == null) { return; } item.setVisibility(VISIBLE); dragging = false; } private int countElements() { int previousElementIndex = 0; for (BrickLayout.LineData line : lines) { for (BrickLayout.ElementData element : line.elements) { if (element.view != null) { previousElementIndex++; } } } return previousElementIndex; } /** * Finds the space closest to x,y where an element can be inserted * * @returns index of the element before the space or -1 for the beginning of the array */ private int findClosestInsertableSpace(int x, int y) { int previousElementIndex = -1; int closestPreviousElementIndex = -1; float closestDistance = 99999999; for (BrickLayout.LineData line : lines) { int elementIndex = 0; for (BrickLayout.ElementData e : line.elements) { if (e.view != null) { float edgeX = e.posX; float edgeY = e.posY; if (e.view.getVisibility() != GONE) { edgeX -= (e.width * 0.5f); } float dx = edgeX - x; float dy = edgeY - y; float d = dx * dx + dy * dy * BIAS_SAME_LINE_FUDGE_FACTOR; if (d < closestDistance) { closestDistance = d; closestPreviousElementIndex = previousElementIndex; } previousElementIndex++; edgeX = e.posX; if (elementIndex == line.elements.size() - 1 || line.elements.get(elementIndex + 1).view == null) { edgeX = (edgeX + (e.width * 0.5f) + getMeasuredWidth()) * 0.5f; } else if (e.view.getVisibility() != GONE) { edgeX += (e.width * 0.5f); } dx = edgeX - x; d = dx * dx + dy * dy * BIAS_SAME_LINE_FUDGE_FACTOR; if (d < closestDistance) { closestDistance = d; closestPreviousElementIndex = previousElementIndex; } elementIndex++; } } } return closestPreviousElementIndex; } private void repositionCursors(int insertableSpaceIndex) { if (dragCursor1 != null && dragCursor1.view != null && insertableSpaceIndex >= 0) { BrickLayout.ElementData previousElement = getElement(insertableSpaceIndex); int rightEdgeOfPreviousElementX = previousElement.posX + previousElement.width; int rightEdgeOfPreviousElementY = previousElement.posY + (int) (previousElement.height * 0.5f); positionWierdFloatingWindow(dragCursor1, rightEdgeOfPreviousElementX, rightEdgeOfPreviousElementY); dragCursor1.view.setVisibility(VISIBLE); } else { dragCursor1.view.setVisibility(GONE); } if (dragCursor2 != null && dragCursor2.view != null && insertableSpaceIndex < countElements() - 1) { BrickLayout.ElementData nextElement = getElement(insertableSpaceIndex + 1); int leftEdgeOfNextElementX = nextElement.posX; int leftEdgeOfNextElementY = nextElement.posY + (int) (nextElement.height * 0.5f); positionWierdFloatingWindow(dragCursor2, leftEdgeOfNextElementX, leftEdgeOfNextElementY); dragCursor2.view.setVisibility(VISIBLE); } else { dragCursor2.view.setVisibility(GONE); } } private BrickLayout.ElementData getElement(int i) { int index = 0; for (BrickLayout.LineData line : lines) { for (BrickLayout.ElementData e : line.elements) { if (e.view != null) { if (index == i) { return e; } index++; } } } return null; } private WeirdFloatingWindowData makeWeirdFloatingWindow(Bitmap bitmap, int width, int height) { Context context = getContext(); ImageView v = new ImageView(context); v.setImageBitmap(bitmap); WindowManager mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mWindowManager.addView(v, getFloatingWindowParams()); return new WeirdFloatingWindowData(v, width, height); } private WeirdFloatingWindowData makeWeirdFloatingWindow(View view) { Context context = getContext(); WindowManager mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mWindowManager.addView(view, getFloatingWindowParams()); return new WeirdFloatingWindowData(view, view.getWidth(), view.getHeight()); } private void positionWierdFloatingWindow(WeirdFloatingWindowData window, int x, int y) { if (window != null && window.view != null) { WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) window.view.getLayoutParams(); int uncenteringX = (int) (ScreenValues.SCREEN_WIDTH * -0.5f) + (int) (window.width * 0.5f); int uncenteringY = (int) (ScreenValues.SCREEN_HEIGHT * -0.5f) + (int) (window.height * 0.5f); layoutParams.x = x + viewToWindowSpaceX + uncenteringX; layoutParams.y = y + viewToWindowSpaceY + uncenteringY; WindowManager mWindowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); mWindowManager.updateViewLayout(window.view, layoutParams); } } private void removeWeirdFloatingWindow(WeirdFloatingWindowData window) { if (window != null && window.view != null) { window.view.setVisibility(GONE); WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); wm.removeView(window.view); } } private WindowManager.LayoutParams getFloatingWindowParams() { WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams(); windowParams.gravity = Gravity.CENTER; windowParams.x = 0; windowParams.y = 0; windowParams.height = LayoutParams.WRAP_CONTENT; windowParams.width = LayoutParams.WRAP_CONTENT; windowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; windowParams.format = PixelFormat.TRANSLUCENT; windowParams.windowAnimations = 0; return windowParams; } private class WeirdFloatingWindowData { public View view; public int width; public int height; public WeirdFloatingWindowData(View view, int width, int height) { this.view = view; this.width = width; this.height = height; } } }