package com.codigoparallevar.minicards; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import com.codigoparallevar.minicards.motion.MotionMode; import com.codigoparallevar.minicards.types.PartConnection; import com.codigoparallevar.minicards.parts.buttons.RoundButton; import com.codigoparallevar.minicards.parts.samples.ColorBox; import com.codigoparallevar.minicards.parts.samples.Placeholder; import com.codigoparallevar.minicards.types.connectors.input.InputConnector; import com.codigoparallevar.minicards.types.connectors.input.SignalInputConnector; import com.codigoparallevar.minicards.types.connectors.output.OutputConnector; import com.codigoparallevar.minicards.types.Part; import com.codigoparallevar.minicards.types.PartGrid; import com.codigoparallevar.minicards.types.Position; import com.codigoparallevar.minicards.types.Selectable; import com.codigoparallevar.minicards.types.Tuple2; import com.codigoparallevar.minicards.types.Tuple4; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; class CanvasView extends View implements PartGrid { @NonNull List parts = new ArrayList<>(); @Nullable Selectable selectedPart; @Nullable final Position lastTouchedPosition = new Position(); @Nullable long lastTouchedTime; @Nullable private MotionMode.Type motionMode; @NonNull private String name = "default"; private final static float touchTimeForLongTouchInMillis = 500; private boolean _isDragging = false; private CardActivity parentActivity = null; private Tuple4 _dropToRemoveZone = new Tuple4<>(0, 0, 0, 0); private boolean _devMode = false; @NonNull private Tuple2 _viewOrigin = new Tuple2<>(0, 0); @Nullable private Tuple2 _mouseDownPoint = null; public CanvasView(Context context) { super(context); } public CanvasView(Context context, AttributeSet attr) { super(context, attr); } public void loadCard(String path) throws IOException, ErrorLoadingCardException { CardFile card = CardFile.load(path, this); name = card.getName(); setBackgroundColor(card.getBackgroundColor()); parts = card.getParts(); List connections = card.getConnections(); resolveConnections(connections); } // This might not be needed here, and probably could be moved to the CardFile. private void resolveConnections(List connections) { Map partsById = buildPartsById(); for (PartConnection connection : connections){ if (!partsById.containsKey(connection.inputPartId)){ Log.e("Canvas view", "Key '" + connection.inputPartId + "' not found on deserialization"); continue; } Part inputPart = partsById.get(connection.inputPartId); InputConnector inputConnector = inputPart.getConnectorWithId(connection.inputConnectorId); if (inputConnector == null){ Log.e("Canvas view", "Connector ID '" + connection.inputConnectorId + "' not found on deserialization"); continue; } OutputConnector outputConnector = connection.outputConnector; outputConnector.connectTo(inputConnector); } } private Map buildPartsById() { Map partsById = new HashMap<>(parts.size()); for (Part part : parts) { partsById.put(part.getId(), part); Log.w("CanvasView", "Added part ID: " + part.getId() + " - " + part); } return partsById; } @Override public void onDraw(Canvas canvas){ final long renderStartTime = System.currentTimeMillis(); ScrolledCanvas scrolledCanvas = new ScrolledCanvas(canvas, _viewOrigin); drawBackground(scrolledCanvas); for (Part part : parts){ part.draw(scrolledCanvas, _devMode); } Log.d("Render time", System.currentTimeMillis() - renderStartTime + "ms"); } private void drawBackground(ScrolledCanvas canvas) { if (!_devMode){ return; } // Blueprint background final int width = getWidth() + getLeft(); final int height = getHeight() + getTop(); final int cellSize = 50; Paint blueprintLines = new Paint(Paint.ANTI_ALIAS_FLAG); blueprintLines.setColor(Color.argb(100, 255, 255, 255)); // Vertical lines for (int x = cellSize; x < width; x += cellSize){ canvas.drawLine(x, 0, x, height, blueprintLines); } // Horizontal lines for (int y = cellSize; y < height; y += cellSize){ canvas.drawLine(0, y, width, y, blueprintLines); } } @Override public boolean onTouchEvent(MotionEvent event){ final int xInScreen = (int) event.getX() - this.getLeft(); final int yInScreen = (int) event.getY() - this.getTop(); final int xInCanvas = xInScreen + _viewOrigin.item1; final int yInCanvas = yInScreen + _viewOrigin.item2; switch (event.getAction()){ case MotionEvent.ACTION_DOWN: { _mouseDownPoint = new Tuple2<>(xInScreen, yInScreen); selectedPart = getPartOn(xInCanvas, yInCanvas); lastTouchedPosition.to(xInScreen, yInScreen); lastTouchedTime = System.currentTimeMillis(); if (selectedPart == null) { Log.d("Touched part", "not found"); return false; } Log.d("Touched part", "Part: " + selectedPart); } break; case MotionEvent.ACTION_UP: { if (motionMode == null){ motionMode = getMotionMode(false); } if (motionMode != MotionMode.Type.LongTouch) { if (selectedPart != null){ if (selectedPart instanceof Part){ ((Part) selectedPart).touched(); } } } else if (motionMode == MotionMode.Type.LongTouch && _devMode) { if (selectedPart != null) { selectedPart.getMoveable().drop(xInCanvas, yInCanvas); if (inDropZone(xInScreen, yInScreen)) { Log.d("Canvas", "Deleting element" + selectedPart); parts.remove(selectedPart); selectedPart.unlink(); } invalidate(); } } _isDragging = false; motionMode = null; selectedPart = null; } try { saveState(); } catch (IOException e) { Log.w("PartCanvasView", e.getMessage()); } break; case MotionEvent.ACTION_MOVE: { if (!_devMode) { break; } if (selectedPart == null){ int xMovement = _mouseDownPoint.item1 - xInScreen; int yMovement = _mouseDownPoint.item2 - yInScreen; _viewOrigin = new Tuple2( _viewOrigin.item1 + xMovement, _viewOrigin.item2 + yMovement); _mouseDownPoint = new Tuple2(xInScreen, yInScreen); } else { Log.i("Canvas", "X: " + xInScreen + " Y: " + yInScreen + " in drop zone " + _dropToRemoveZone + " : " + inDropZone(xInScreen, yInScreen)); if (motionMode == null) { final Selectable nowSelectedPart = getPartOn(xInCanvas, yInCanvas); final boolean canWait = selectedPart == nowSelectedPart; motionMode = getMotionMode(canWait); } if (motionMode == MotionMode.Type.LongTouch) { if (selectedPart != null) { _isDragging = true; selectedPart.getMoveable().moveEnd(xInCanvas, yInCanvas); invalidate(); } } } } break; default: { Log.d("PartCanvasView", "Unhandled action: " + event.getAction()); } } invalidate(); if (parentActivity != null) { parentActivity.invalidate(); } return true; } private boolean inDropZone(int x, int y) { return (x >= _dropToRemoveZone._x1) && (x <= _dropToRemoveZone._x2) && (y >= _dropToRemoveZone._y1) && (y <= _dropToRemoveZone._y2); } private void saveState() throws IOException { CardFile cardFile = new CardFile(CardFile.getDefaultCardStorage(getContext())); cardFile.setName(name); cardFile.addParts(parts); cardFile.save(getContext()); } @Nullable private MotionMode.Type getMotionMode(boolean canWait) { if (selectedPart == null){ return MotionMode.Type.Displacement; } if ((System.currentTimeMillis() - lastTouchedTime) >= touchTimeForLongTouchInMillis){ return MotionMode.Type.LongTouch; } if (canWait) { return null; } return MotionMode.Type.ShortTouch; } @Nullable public Selectable getPartOn(int x, int y) { // Look in the list of parts, in reverse so top-most elements are checked before for (int i = parts.size() - 1; i >= 0; i--){ final Part part = parts.get(i); if (part.containsPoint(x, y)){ return part; } } // If no part was found, do the same for connectors for (int i = parts.size() - 1; i >= 0; i--){ final Part part = parts.get(i); // First try with output connectors for (OutputConnector outputConnector : part.getOutputConnectors()){ if (outputConnector.containsPoint(x, y)){ return outputConnector; } } // Then with input ones for (InputConnector inputConnector : part.getSignalInputConnectors()){ if (inputConnector.containsPoint(x, y)){ return inputConnector; } } } return null; } @Override @Nullable public SignalInputConnector getSignalInputConnectorOn(int x, int y) { // If no part was found, do the same for connectors for (int i = parts.size() - 1; i >= 0; i--){ final Part part = parts.get(i); // Then with input ones for (SignalInputConnector inputConnector : part.getSignalInputConnectors()){ if (inputConnector.containsPoint(x, y)){ return inputConnector; } } } return null; } public void addPart(Part part) { parts.add(part); invalidate(); } public boolean isDragging() { return _isDragging; } public void setParentActivity(CardActivity parentActivity) { this.parentActivity = parentActivity; } public void setDropZone(float x1, float x2, float y1, float y2) { _dropToRemoveZone = new Tuple4<>((int) x1, (int) x2, (int) y1, (int) y2); } public void setDevMode(boolean devMode) { _devMode = devMode; if (!_devMode) { CenterView(); } this.invalidate(); } public void CenterView() { this._viewOrigin = new Tuple2<>(0, 0); this.invalidate(); } public Tuple2 getCenteredOn() { int xOffset = _viewOrigin.item1 + getWidth() / 2; int yOffset = _viewOrigin.item2 + getHeight() / 2; return new Tuple2<>(xOffset, yOffset); } }