我有这个练习项目,允许用户在用手指触摸时在屏幕上绘图。我做的非常简单的应用程序作为锻炼方式。我的小表弟在这个应用程序上用我的 iPad 随意用手指画东西(儿童画:圆圈、线条等,无论他想到什么)。然后他开始画圆,然后他让我把它做成一个“好圆”(根据我的理解:把画的圆画成完美的圆,因为我们知道无论我们用手指在屏幕上画的东西有多稳定,一个圆圈从来没有真正像一个圆圈那样圆)。
所以我的问题是,在代码中是否有任何方法可以首先检测用户绘制的形成一个圆圈的线,并通过使其在屏幕上完美地变圆来生成大致相同大小的圆圈。让一条不太直线的直线是我知道该怎么做的,但至于圆,我不太知道如何用 Quartz 或其他方法去做。
我的推理是,在用户抬起手指后,线的起点和终点必须相互接触或交叉,以证明他实际上是在画一个圆。
有时花一些时间重新发明轮子真的很有用。您可能已经注意到有很多框架,但是在不引入所有复杂性的情况下实现一个简单但有用的解决方案并不难。(请不要误会我的意思,出于任何严肃的目的,最好使用一些成熟且被证明是稳定的框架)。
我将首先介绍我的结果,然后解释它们背后简单明了的想法。
您会在我的实现中看到,无需分析每个点并进行复杂的计算。这个想法是发现一些有价值的元信息。我将以切线为例:
让我们确定一个简单直接的模式,典型的选定形状:
所以基于这个想法来实现一个圆检测机制并不难。请参阅下面的工作演示(抱歉,我使用 Java 作为提供这个快速且有点脏的示例的最快方法):
import java.awt.BasicStroke; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.HeadlessException; import java.awt.Point; import java.awt.RenderingHints; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.ArrayList; import java.util.List; import javax.swing.JFrame; import javax.swing.SwingUtilities; public class CircleGestureDemo extends JFrame implements MouseListener, MouseMotionListener { enum Type { RIGHT_DOWN, LEFT_DOWN, LEFT_UP, RIGHT_UP, UNDEFINED } private static final Type[] circleShape = { Type.RIGHT_DOWN, Type.LEFT_DOWN, Type.LEFT_UP, Type.RIGHT_UP}; private boolean editing = false; private Point[] bounds; private Point last = new Point(0, 0); private List<Point> points = new ArrayList<>(); public CircleGestureDemo() throws HeadlessException { super("Detect Circle"); addMouseListener(this); addMouseMotionListener(this); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setPreferredSize(new Dimension(800, 600)); pack(); } @Override public void paint(Graphics graphics) { Dimension d = getSize(); Graphics2D g = (Graphics2D) graphics; super.paint(g); RenderingHints qualityHints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); qualityHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.setRenderingHints(qualityHints); g.setColor(Color.RED); if (cD == 0) { Point b = null; for (Point e : points) { if (null != b) { g.drawLine(b.x, b.y, e.x, e.y); } b = e; } }else if (cD > 0){ g.setColor(Color.BLUE); g.setStroke(new BasicStroke(3)); g.drawOval(cX, cY, cD, cD); }else{ g.drawString("Uknown",30,50); } } private Type getType(int dx, int dy) { Type result = Type.UNDEFINED; if (dx > 0 && dy < 0) { result = Type.RIGHT_DOWN; } else if (dx < 0 && dy < 0) { result = Type.LEFT_DOWN; } else if (dx < 0 && dy > 0) { result = Type.LEFT_UP; } else if (dx > 0 && dy > 0) { result = Type.RIGHT_UP; } return result; } private boolean isCircle(List<Point> points) { boolean result = false; Type[] shape = circleShape; Type[] detected = new Type[shape.length]; bounds = new Point[shape.length]; final int STEP = 5; int index = 0; Point current = points.get(0); Type type = null; for (int i = STEP; i < points.size(); i += STEP) { Point next = points.get(i); int dx = next.x - current.x; int dy = -(next.y - current.y); if(dx == 0 || dy == 0) { continue; } Type newType = getType(dx, dy); if(type == null || type != newType) { if(newType != shape[index]) { break; } bounds[index] = current; detected[index++] = newType; } type = newType; current = next; if (index >= shape.length) { result = true; break; } } return result; } @Override public void mousePressed(MouseEvent e) { cD = 0; points.clear(); editing = true; } private int cX; private int cY; private int cD; @Override public void mouseReleased(MouseEvent e) { editing = false; if(points.size() > 0) { if(isCircle(points)) { cX = bounds[0].x + Math.abs((bounds[2].x - bounds[0].x)/2); cY = bounds[0].y; cD = bounds[2].y - bounds[0].y; cX = cX - cD/2; System.out.println("circle"); }else{ cD = -1; System.out.println("unknown"); } repaint(); } } @Override public void mouseDragged(MouseEvent e) { Point newPoint = e.getPoint(); if (editing && !last.equals(newPoint)) { points.add(newPoint); last = newPoint; repaint(); } } @Override public void mouseMoved(MouseEvent e) { } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } @Override public void mouseClicked(MouseEvent e) { } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { CircleGestureDemo t = new CircleGestureDemo(); t.setVisible(true); } }); } }
在 iOS 上实现类似的行为应该不是问题,因为您只需要几个事件和坐标。类似于以下内容(参见示例):
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch* touch = [[event allTouches] anyObject]; } - (void)handleTouch:(UIEvent *)event { UITouch* touch = [[event allTouches] anyObject]; CGPoint location = [touch locationInView:self]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [self handleTouch: event]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [self handleTouch: event]; }
有几种可能的增强功能。
从任何时候开始
由于以下简化,当前要求是从顶部中间点开始画一个圆:
if(type == null || type != newType) { if(newType != shape[index]) { break; } bounds[index] = current; detected[index++] = newType; }
请注意使用默认值index。通过形状的可用“部分”进行简单搜索将消除该限制。请注意,您需要使用循环缓冲区来检测完整的形状:
index
顺时针和逆时针
为了支持这两种模式,您需要使用先前增强中的循环缓冲区并在两个方向上搜索:
画一个椭圆
您已经在bounds阵列中拥有了所需的一切。
bounds
只需使用该数据:
cWidth = bounds[2].y - bounds[0].y; cHeight = bounds[3].y - bounds[1].y;
其他手势(可选)
最后,您只需要正确处理dx(或dy)等于零的情况,以支持其他手势:
dx
dy
更新
这个小 PoC 得到了相当高的关注,所以我确实更新了一些代码以使其运行顺畅并提供一些绘图提示,突出支持点等:
这是代码:
import java.awt.BasicStroke; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.HeadlessException; import java.awt.Point; import java.awt.RenderingHints; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.ArrayList; import java.util.List; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.SwingUtilities; public class CircleGestureDemo extends JFrame { enum Type { RIGHT_DOWN, LEFT_DOWN, LEFT_UP, RIGHT_UP, UNDEFINED } private static final Type[] circleShape = { Type.RIGHT_DOWN, Type.LEFT_DOWN, Type.LEFT_UP, Type.RIGHT_UP}; public CircleGestureDemo() throws HeadlessException { super("Circle gesture"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new BorderLayout()); add(BorderLayout.CENTER, new GesturePanel()); setPreferredSize(new Dimension(800, 600)); pack(); } public static class GesturePanel extends JPanel implements MouseListener, MouseMotionListener { private boolean editing = false; private Point[] bounds; private Point last = new Point(0, 0); private final List<Point> points = new ArrayList<>(); public GesturePanel() { super(true); addMouseListener(this); addMouseMotionListener(this); } @Override public void paint(Graphics graphics) { super.paint(graphics); Dimension d = getSize(); Graphics2D g = (Graphics2D) graphics; RenderingHints qualityHints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); qualityHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.setRenderingHints(qualityHints); if (!points.isEmpty() && cD == 0) { isCircle(points, g); g.setColor(HINT_COLOR); if (bounds[2] != null) { int r = (bounds[2].y - bounds[0].y) / 2; g.setStroke(new BasicStroke(r / 3 + 1)); g.drawOval(bounds[0].x - r, bounds[0].y, 2 * r, 2 * r); } else if (bounds[1] != null) { int r = bounds[1].x - bounds[0].x; g.setStroke(new BasicStroke(r / 3 + 1)); g.drawOval(bounds[0].x - r, bounds[0].y, 2 * r, 2 * r); } } g.setStroke(new BasicStroke(2)); g.setColor(Color.RED); if (cD == 0) { Point b = null; for (Point e : points) { if (null != b) { g.drawLine(b.x, b.y, e.x, e.y); } b = e; } } else if (cD > 0) { g.setColor(Color.BLUE); g.setStroke(new BasicStroke(3)); g.drawOval(cX, cY, cD, cD); } else { g.drawString("Uknown", 30, 50); } } private Type getType(int dx, int dy) { Type result = Type.UNDEFINED; if (dx > 0 && dy < 0) { result = Type.RIGHT_DOWN; } else if (dx < 0 && dy < 0) { result = Type.LEFT_DOWN; } else if (dx < 0 && dy > 0) { result = Type.LEFT_UP; } else if (dx > 0 && dy > 0) { result = Type.RIGHT_UP; } return result; } private boolean isCircle(List<Point> points, Graphics2D g) { boolean result = false; Type[] shape = circleShape; bounds = new Point[shape.length]; final int STEP = 5; int index = 0; int initial = 0; Point current = points.get(0); Type type = null; for (int i = STEP; i < points.size(); i += STEP) { final Point next = points.get(i); final int dx = next.x - current.x; final int dy = -(next.y - current.y); if (dx == 0 || dy == 0) { continue; } final int marker = 8; if (null != g) { g.setColor(Color.BLACK); g.setStroke(new BasicStroke(2)); g.drawOval(current.x - marker/2, current.y - marker/2, marker, marker); } Type newType = getType(dx, dy); if (type == null || type != newType) { if (newType != shape[index]) { break; } bounds[index++] = current; } type = newType; current = next; initial = i; if (index >= shape.length) { result = true; break; } } return result; } @Override public void mousePressed(MouseEvent e) { cD = 0; points.clear(); editing = true; } private int cX; private int cY; private int cD; @Override public void mouseReleased(MouseEvent e) { editing = false; if (points.size() > 0) { if (isCircle(points, null)) { int r = Math.abs((bounds[2].y - bounds[0].y) / 2); cX = bounds[0].x - r; cY = bounds[0].y; cD = 2 * r; } else { cD = -1; } repaint(); } } @Override public void mouseDragged(MouseEvent e) { Point newPoint = e.getPoint(); if (editing && !last.equals(newPoint)) { points.add(newPoint); last = newPoint; repaint(); } } @Override public void mouseMoved(MouseEvent e) { } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } @Override public void mouseClicked(MouseEvent e) { } } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { CircleGestureDemo t = new CircleGestureDemo(); t.setVisible(true); } }); } final static Color HINT_COLOR = new Color(0x55888888, true); }