Android实时绘图应用:Firebase RTDB中Path数据的存储类型与键设计
Great question! Since Firebase Realtime Database can't directly serialize Android's Path object (it's platform-specific and doesn't play nice with Firebase's supported data types), you'll need to convert your paths into a serializable format first, then structure your keys to fit your app's use case. Let's break this down step by step:
1. Convert Path to a Serializable Model
You have two main options here, depending on whether you need to support complex paths (like curves or bezier shapes) or just basic lines:
Option A: Full Path Operation Tracking (Supports All Path Types)
This method records every operation that builds the Path (moveTo, lineTo, quadTo, etc.), which lets you fully reconstruct the original path later. Create two simple model classes to hold this data:
// PathModel stores the full path data public class PathModel { private List<PathOperation> operations; private int fillType; // Required empty constructor for Firebase deserialization public PathModel() {} public PathModel(Path path) { this.fillType = path.getFillType().ordinal(); this.operations = new ArrayList<>(); // Iterate through the path to extract all operations PathIterator iterator = path.getPathIterator(null); float[] coords = new float[6]; while (!iterator.isDone()) { int type = iterator.currentSegment(coords); switch (type) { case PathIterator.SEG_MOVETO: operations.add(new PathOperation("moveTo", coords[0], coords[1])); break; case PathIterator.SEG_LINETO: operations.add(new PathOperation("lineTo", coords[0], coords[1])); break; case PathIterator.SEG_QUADTO: operations.add(new PathOperation("quadTo", coords[0], coords[1], coords[2], coords[3])); break; case PathIterator.SEG_CUBICTO: operations.add(new PathOperation("cubicTo", coords[0], coords[1], coords[2], coords[3], coords[4], coords[5])); break; case PathIterator.SEG_CLOSE: operations.add(new PathOperation("close")); break; } iterator.next(); } } // Convert PathModel back to Android Path public Path toAndroidPath() { Path path = new Path(); path.setFillType(Path.FillType.values()[fillType]); for (PathOperation op : operations) { switch (op.getType()) { case "moveTo": path.moveTo(op.getX(), op.getY()); break; case "lineTo": path.lineTo(op.getX(), op.getY()); break; case "quadTo": path.quadTo(op.getX(), op.getY(), op.getCtrlX(), op.getCtrlY()); break; case "cubicTo": path.cubicTo(op.getX(), op.getY(), op.getCtrlX(), op.getCtrlY(), op.getEndX(), op.getEndY()); break; case "close": path.close(); break; } } return path; } // Getters and setters (required for Firebase) public List<PathOperation> getOperations() { return operations; } public void setOperations(List<PathOperation> operations) { this.operations = operations; } public int getFillType() { return fillType; } public void setFillType(int fillType) { this.fillType = fillType; } } // PathOperation stores individual path commands public class PathOperation { private String type; private float x, y; private float ctrlX, ctrlY; private float endX, endY; public PathOperation() {} // Constructors for different operation types public PathOperation(String type) { this.type = type; } public PathOperation(String type, float x, float y) { this.type = type; this.x = x; this.y = y; } public PathOperation(String type, float x, float y, float ctrlX, float ctrlY) { this.type = type; this.x = x; this.y = y; this.ctrlX = ctrlX; this.ctrlY = ctrlY; } public PathOperation(String type, float x, float y, float ctrlX, float ctrlY, float endX, float endY) { this.type = type; this.x = x; this.y = y; this.ctrlX = ctrlX; this.ctrlY = ctrlY; this.endX = endX; this.endY = endY; } // Getters and setters public String getType() { return type; } public void setType(String type) { this.type = type; } public float getX() { return x; } public void setX(float x) { this.x = x; } // Add getters/setters for other fields... }
Option B: Simple Point List (For Basic Line Paths)
If your app only draws straight lines, you can simplify things by storing just the list of points in the path. This uses less storage and is easier to implement:
public class SimplePathModel { private List<Point> points; private int fillType; public SimplePathModel() {} public SimplePathModel(Path path) { this.fillType = path.getFillType().ordinal(); this.points = new ArrayList<>(); PathIterator iterator = path.getPathIterator(null); float[] coords = new float[2]; while (!iterator.isDone()) { int type = iterator.currentSegment(coords); if (type == PathIterator.SEG_MOVETO || type == PathIterator.SEG_LINETO) { points.add(new Point(coords[0], coords[1])); } iterator.next(); } } public Path toAndroidPath() { Path path = new Path(); path.setFillType(Path.FillType.values()[fillType]); if (!points.isEmpty()) { Point first = points.get(0); path.moveTo(first.getX(), first.getY()); for (int i = 1; i < points.size(); i++) { Point p = points.get(i); path.lineTo(p.getX(), p.getY()); } } return path; } // Getters and setters public List<Point> getPoints() { return points; } public void setPoints(List<Point> points) { this.points = points; } public int getFillType() { return fillType; } public void setFillType(int fillType) { this.fillType = fillType; } } public class Point { private float x, y; public Point() {} public Point(float x, float y) { this.x = x; this.y = y; } // Getters and setters public float getX() { return x; } public void setX(float x) { this.x = x; } public float getY() { return y; } public void setY(float y) { this.y = y; } }
2. Structure Your Firebase Keys
The key structure depends on whether you're building a single-user app or a collaborative multi-user app:
For Collaborative Multi-User Drawing
Use Firebase's push() method to generate unique, auto-incrementing keys for each path. This ensures no conflicts when multiple users add paths at the same time. Your Firebase structure might look like this:
drawings - {drawingId} // Generated via push() or custom ID for each drawing - paths - -Nabc123: { operations: [...], fillType: 0 } // Path 1 - -Ndef456: { operations: [...], fillType: 0 } // Path 2
To add a path:
DatabaseReference pathsRef = FirebaseDatabase.getInstance() .getReference("drawings/" + drawingId + "/paths"); // Convert your Android Path to PathModel PathModel pathModel = new PathModel(yourCanvasPath); // Push to generate a unique key and save the path String pathKey = pathsRef.push().getKey(); pathsRef.child(pathKey).setValue(pathModel);
For Single-User Undo/Redo
Even for single-user apps, avoid using numeric indexes as keys (Firebase will reorder indexes if you delete a middle element). Instead, still use push() keys, and maintain a local List<String> to track the order of path keys. This makes undoing the last path straightforward:
// Local variables to track paths and their Firebase keys private ArrayList<Path> localPaths = new ArrayList<>(); private ArrayList<String> pathKeys = new ArrayList<>(); private DatabaseReference userPathsRef; // Initialize the reference (replace with your user ID logic) userPathsRef = FirebaseDatabase.getInstance() .getReference("userDrawings/" + currentUserId + "/paths"); // When adding a new path: public void addPath(Path newPath) { localPaths.add(newPath); PathModel pathModel = new PathModel(newPath); String newKey = userPathsRef.push().getKey(); pathKeys.add(newKey); userPathsRef.child(newKey).setValue(pathModel); } // When undoing the last path: public void undoLastPath() { if (!localPaths.isEmpty() && !pathKeys.isEmpty()) { // Remove from local list localPaths.remove(localPaths.size() - 1); // Remove from Firebase String lastKey = pathKeys.remove(pathKeys.size() - 1); userPathsRef.child(lastKey).removeValue(); } }
3. Sync Local and Firebase State
To keep your Canvas in sync with Firebase, add a listener to the paths reference. When a path is added or removed, update your local ArrayList<Path> and redraw the Canvas:
userPathsRef.addChildEventListener(new ChildEventListener() { @Override public void onChildAdded(@NonNull DataSnapshot snapshot, @Nullable String previousChildName) { PathModel pathModel = snapshot.getValue(PathModel.class); if (pathModel != null) { Path path = pathModel.toAndroidPath(); localPaths.add(path); // Trigger Canvas redraw yourCanvasView.invalidate(); } } @Override public void onChildRemoved(@NonNull DataSnapshot snapshot) { // Find and remove the corresponding path from local list String removedKey = snapshot.getKey(); int index = pathKeys.indexOf(removedKey); if (index != -1) { localPaths.remove(index); pathKeys.remove(index); yourCanvasView.invalidate(); } } // Implement other ChildEventListener methods as needed... });
内容的提问来源于stack exchange,提问作者WHOATEMYNOODLES

