Detailed explanation of the code that implements the WordPad function based on Android
1. Project introduction
1. Background and application scenarios
In many application scenarios, we need to allow users to freely draw or handwriting input, such as:
Signature confirmation: Electronic contract, express delivery signing
Drawing graffiti:Social App Share Hand-drawn Content
Apply and erase: Children's Educational Painting
Tag annotation: Map/picture marks, document annotations
This project will implement a highly customizable writing board that meets:
Free drawing: Supports multiple strokes, multiple colors, and multiple thicknesses
Undo redo: Revocable/redo operation
Clear screen save: Clear one click, save as picture with one click
Gesture optimization: Smooth curve, pressure sensing simulation (molarity simulation)
UI customizable: Color panel, pen width control, clear/undo/save buttons
Componentization: Packaging
DrawingBoardView
, easy to use in any layout
2. Function list
Draw paths: Users touch the screen to draw continuous curves in real time
Multi-color switching: Provide a color palette, support any color
Adjustable pen width: Supports at least 3 stroke thicknesses
Undo/redo: Can undo and redo each path
Clear the canvas: Clear all drawn content with one click
Save the picture: Save canvas content to local album or apply a private directory
Export and share: You can directly share the drawn pictures
Performance optimization: Supports hardware acceleration, path caching, and local refresh
2. Related knowledge
Before you start, you need to understand the following core technical points:
-
Custom Views and Canvas
Rewrite
onDraw(Canvas)
,use(Path, Paint)
Draw pathexist
onTouchEvent(MotionEvent)
China basisACTION_DOWN/MOVE/UP
BuildPath
-
Data structure and undo/redo
use
List<Path>
Save the completed path and useStack<Path>
Save the undoed path to support redoingAfter each transaction is completed,
currentPath
join inpaths
, clearredoStack
-
Performance optimization
cache
Path
andPaint
Objects, avoid frequent allocationexist
invalidate(Rect)
Refresh the touch area locally to reduce full-screen redrawing
-
Touch smooth
Use quadratic Bezier curves to smooth the trajectory:
(prevX, prevY, (x+prevX)/2, (y+prevY)/2)
-
File saving and sharing
Will
Bitmap
Export: inDrawingBoardView
Generated inBitmap
andCanvas
Draw the base map and all paths at onceuse
MediaStore
(Android Q+) orFileOutputStream
Save to albumuse
FileProvider
andIntent.ACTION_SEND
Share pictures
-
UI Components
use
RecyclerView
orLinearLayout
Build color panels and pen width panelsuse
MaterialButton
、FloatingActionButton
Such bearer undo, redo, clear, save operations
Ideas for realization
-
Packaging
DrawingBoardView
Public properties:
setStrokeColor(int)
,setStrokeWidth(float)
,undo()
,redo()
,clear()
,exportBitmap()
Event handling:
onTouchEvent
Collect and smoothly record the touch trajectory;
-
Main interface layout
Top button area: Undo, redo, clear, save
Central
DrawingBoardView
Fully screenBottom toolbar: color selection, pen width slider
-
File storage and sharing
exist
MainActivity
Called in()
GetBitmap
, save or shareUse coroutines or background threads to process I/O to display progress prompts
-
Status saving and recovery
exist
onSaveInstanceState
Savepaths
andredoStack
Serialized dataexist
onRestoreInstanceState
Restore the path to avoid loss of drawings on the screen rotation
-
Modularization and multiplexing
Encapsulate all drawing logic in
Encapsulate the save and share functions in
4. Environment and dependence
// app/ apply plugin: '' apply plugin: 'kotlin-android' android { compileSdkVersion 34 defaultConfig { applicationId "" minSdkVersion 21 targetSdkVersion 34 } buildFeatures { viewBinding true } kotlinOptions { jvmTarget = "1.8" } } dependencies { implementation ':appcompat:1.6.1' implementation ':core-ktx:1.10.1' implementation ':material:1.9.0' implementation ':lifecycle-runtime-ktx:2.6.1' }
5. Integrate code
// ======================================================= // File: res/layout/activity_main.xml// Description: Main interface layout, including toolbar, DrawingBoardView, color/pen width tools// ======================================================= <?xml version="1.0" encoding="utf-8"?> < xmlns:andro xmlns:app="/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- Top Action Bar --> < android: android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:theme="@style/" app:title="Writing Board"/> <!-- Draw the panel --> < android: android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="?attr/actionBarSize" android:background="#FFFFFF"/> <!-- Bottom toolbar --> <LinearLayout android: android:orientation="horizontal" android:gravity="center_vertical" android:padding="8dp" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:background="#CCFFFFFF"> <!-- Color panel --> <HorizontalScrollView android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content"> <LinearLayout android: android:orientation="horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </HorizontalScrollView> <!-- Pen width slider --> <SeekBar android: android:layout_width="120dp" android:layout_height="wrap_content" android:max="50" android:progress="10" android:layout_marginStart="16dp"/> </LinearLayout> <!-- Floating operation button --> < android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" android:src="@drawable/ic_baseline_clear_24" app:layout_anchorGravity="bottom|end" app:layout_anchor="@id/drawingBoard"/> < android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" android:src="@drawable/ic_baseline_undo_24" app:layout_anchorGravity="bottom|start" app:layout_anchor="@id/drawingBoard"/> < android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" android:src="@drawable/ic_baseline_redo_24" app:layout_anchorGravity="bottom|start" app:layout_anchor="@id/btnUndo"/> < android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" android:src="@drawable/ic_baseline_save_24" app:layout_anchorGravity="bottom|end" app:layout_anchor="@id/btnClear"/> </> // ======================================================= // document:// Description: Custom drawing board, supports drawing, undo, redo, clear, and export// ======================================================= package import import .* import import import import .* class DrawingBoardView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : View(context, attrs) { // Collection of brushes and paths private var paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = ; strokeWidth = 10f style = ; strokeCap = strokeJoin = } private var currentPath = Path() private val paths = mutableListOf<Pair<Path, Paint>>() private val redoStack = Stack<Pair<Path, Paint>>() // Touch the previous point private var prevX = 0f; private var prevY = 0f /** Set the brush color */ fun setStrokeColor(color: Int) { = color } /** Set the brush thickness */ fun setStrokeWidth(width: Float) { = width } /** Undo */ fun undo() { if (()) (()) invalidate() } /** Redo */ fun redo() { if (()) paths += () invalidate() } /** Clear */ fun clear() { (); () invalidate() } /** Export Bitmap */ fun exportBitmap(): Bitmap { val bmp = (width, height, .ARGB_8888) val canvas = Canvas(bmp) () for ((p, paint) in paths) (p, paint) return bmp } override fun onTouchEvent(e: MotionEvent): Boolean { val x = ; val y = when () { MotionEvent.ACTION_DOWN -> { currentPath = Path().apply { moveTo(x, y) } prevX = x; prevY = y // Clear the redo stack with new operation () } MotionEvent.ACTION_MOVE -> { val mx = (x + prevX) / 2 val my = (y + prevY) / 2 (prevX, prevY, mx, my) prevX = x; prevY = y } MotionEvent.ACTION_UP -> { // Complete a stroke and store the path and its brush attributes val p = Path(currentPath) val paintCopy = Paint(paint) paths += Pair(p, paintCopy) } } invalidate() return true } override fun onDraw(canvas: Canvas) { (canvas) // Draw historical paths in turn for ((p, paint) in paths) (p, paint) // Draw the current path (currentPath, paint) } } // ======================================================= // document:// Description: Image saving and sharing tool// ======================================================= package import import import import import import .* object ImageUtil { /** Save to album and return to Uri */ fun saveBitmapToGallery(ctx: Context, bmp: Bitmap, name: String = "draw_${()}"): Uri? { return if (.SDK_INT >= Build.VERSION_CODES.Q) { val values = ContentValues().apply { put(.DISPLAY_NAME, "$") put(.MIME_TYPE, "image/png") put(.RELATIVE_PATH, "Pictures/DrawingBoard") put(.IS_PENDING, 1) } val uri = (.EXTERNAL_CONTENT_URI, values) uri?.let { (it)?.use { os -> (, 100, os) } (); (.IS_PENDING, 0) (it, values, null, null) } uri } else { val dir = File((null), "DrawingBoard") if (!()) () val file = File(dir, "$") FileOutputStream(file).use { fos -> (, 100, fos) } (file) } } } // ======================================================= // document:// Description: Main interface logic: Initialize artboard, tool binding, save and share// ======================================================= package import import import import import import import import import import import .* class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private val scope = CoroutineScope( + Job()) // Temporary Uri after sharing private var savedImageUri: Uri? = null // Share authorization private val shareLauncher = registerForActivityResult( () ) { /* nothing */ } override fun onCreate(savedInstanceState: Bundle?) { (savedInstanceState) binding = (layoutInflater) setContentView() // Initialize the color panel initColorPalette() // Pen width control (object: SimpleSeekListener(){ override fun onProgressChanged(sb: , p: Int, u: Boolean) { (()) } }) // Top button binding { () } { () } { () } { saveDrawing() } } private fun initColorPalette() { val colors = listOf(, , , , ) for (c in colors) { val btn = ImageButton(this).apply { val size = (.color_btn_size) layoutParams = (size, size).apply { marginEnd = 16 } setBackgroundColor(c) setOnClickListener { (c) } } (btn) } } private fun saveDrawing() { // Save and share asynchronously { val bmp = withContext() { () } savedImageUri = (this@MainActivity, bmp) if (savedImageUri != null) { shareImage(savedImageUri!!) } else { (this@MainActivity, "Save failed", Toast.LENGTH_SHORT).show() } } } private fun shareImage(uri: Uri) { val contentUri = if ( == "file") { (this, "$", (!!).toFile()) } else uri val intent = Intent(Intent.ACTION_SEND).apply { type = "image/png" putExtra(Intent.EXTRA_STREAM, contentUri) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } ((intent, "Share drawing")) } override fun onDestroy() { () () } } // ======================================================= // document:// Description: Simple SeekBar listening, omit callback implementation// ======================================================= package import abstract class SimpleSeekListener: { override fun onStartTrackingTouch(p0: SeekBar?) {} override fun onStopTrackingTouch(p0: SeekBar?) {} }
6. Code interpretation
-
DrawingBoardView
Data structure:
paths: List<Pair<Path,Paint>>
Save each stroke trajectory and the corresponding brush;Touch processing:use
quadTo
Smooth drawing; inACTION_UP
Deep copy path and brush entrypaths
;Undo/redo:
undo()
frompaths
Move out the last transactionredoStack
;redo()
Then reverse operation;Clear and export:
clear()
Clear everything,exportBitmap()
Generate white backgroundBitmap
And redraw all paths.
-
ImageUtil
Compatible with Android Q+ and the following versions, use them separately
MediaStore
or file stream saving;Save in
Pictures/DrawingBoard
orgetExternalFilesDir
, and returnUri
Easy to share.
-
MainActivity
UI binding:
colorPalette
Dynamically generate color buttons,seekStroke
Dynamically control pen width;Operation button: Clear, undo, and redo buttons to directly call the corresponding API;
Save and share: Coroutine asynchronous export
Bitmap
→Save →GetUri
→ByIntent.ACTION_SEND
share;
-
Permissions and URIs
use
FileProvider
Adapt to Android 7.0+ file access restrictions;exist
and
provider_paths.xml
Correct configuration;
7. Performance and Optimization
-
Partial refresh
Available in
onTouchEvent
Record the change area in the useinvalidate(left, top, right, bottom)
Replace global refresh;
-
Object reuse
Avoid creating new ones every time you touch
Paint
orPath
Objects, can maintain pooling strategies;
-
Memory management
For large canvases or long-term drawing, pay attention to Bitmap memory and use it if necessary
inBitmap
Reuse;
-
Multi-touch
Extended to support multiple finger drawing at the same time, one per finger
Path
;
8. Project Summary and Expansion
This article fully implements a complete writing board component, covering the entire process of free drawing, undoing and redoing, clearing, saving and sharing.
Through component encapsulation, the business layer only needs to reference it in the layout.
DrawingBoardView
And bind the button to quickly integrate.
Expand direction
Pen pressure sensing: Dynamically adjust the width or transparency of the pen in combination with the pressure of the stylus;
Graphic marking: Supports various annotation modes such as straight lines, rectangles, circles, and text;
Cloud synchronization: Upload the drawing data to the server in vector format to achieve cross-end synchronization;
Animation playback: Record drawing timestamps, supporting playback of the drawing process;
Jetpack Compose Refactoring:use
Canvas
andImplement the Compose version of the writing board.
9. FAQ
Q: How to save multi-page canvas?
A: Can be found inpaths
Add to page index, generate multiple photos according to page numbers when exporting.Bitmap
And pack.Q: What should I do if the image is too large after Bitmap is exported?
A: When savingBitmap
Compress, or scale to the appropriate size first.Q: How to make the undo support some handwriting?
A: Currently, it is cancelled by the whole piece. If you need to cancel it in detail, you can use each paragraph.quadTo
Split into smaller paths and record.Q: How to keep drawing after rotating the screen?
A: InonSaveInstanceState
Serializationpaths
Data, after rotationonRestoreInstanceState
Recovery.Q: How to support the graffiti eraser function?
A: You can switch in graffiti mode= PorterDuffXfermode()
To erase the track.
The above is the detailed explanation of the code that implements the WordPad function based on Android. For more information about the Android WordPad function, please pay attention to my other related articles!
Related Articles
Detailed tutorial on the implementation of Android's open-screen page countdown function
This article mainly introduces a detailed tutorial on the implementation of the countdown function of Android for opening the screen page, which has certain reference value. Interested friends can refer to it.2017-06-06Android ViewPager and radiogroup implementation association example
This article mainly introduces the examples of the implementation association of Android ViewPager and radiogroup, which has certain reference value. If you are interested, you can learn about it.2017-03-03Android implements the function of adding two numbers
This article mainly introduces the implementation of two numbers addition function for Android. The sample code in the article is introduced in detail and has a certain reference value. Interested friends can refer to it.2020-03-03How to use Snackbar in Android and tips
This article mainly introduces relevant materials on how to use Snackbar in Android and the tips. The example code is introduced in this article in detail, which has certain reference learning value for everyone's study or work. Friends who need it, let's study together.2018-03-03Detailed explanation of how to implement list slip removal by Android programming
This article mainly introduces the method of Android programming to implement list slip removal. Combined with the example form, it analyzes the principles and specific implementation techniques of Android list slip removal function. The comments include detailed instructions. Friends who need it can refer to it.2018-01-01Detailed explanation of Android teaches you how to create an efficient picture loading framework
This article mainly introduces a detailed explanation of Android and teaches you how to create an efficient picture loading framework. The editor thinks it is quite good. Now I will share it with you and give you a reference. Let's take a look with the editor2016-12-12
Code for parsing JSON data in Android
This article mainly introduces the relevant information on the JSON data analysis instance code in Android. Friends who need it can refer to it2017-03-03Android programming development method of seekBar using handler message processing operation
This article mainly introduces the method of seekBar using handler message processing operations in Android programming development. It analyzes the relevant skills of Android to implement progress bar functions based on examples. Friends who need it can refer to it.2015-12-12Android imitation WeChat voice recording function
This article mainly introduces the function of recording voice in Android imitation WeChat. The sample code in the article is introduced in detail and has certain reference value. Interested friends can refer to it.2019-11-11Android Animation Practical Battle PopupWindow pops up at the bottom of the screen
This article mainly introduces Android Animation animation practical project. PopupWindow pops up at the bottom of the screen. How to implement it? The sample code in the article is introduced in detail and has certain reference value. Interested friends can refer to it.2016-01-01