AztecEditor-Android开发实战:自定义工具栏与Task List功能实现教程
2026/7/18 10:43:27 网站建设 项目流程

AztecEditor-Android开发实战:自定义工具栏与Task List功能实现教程

【免费下载链接】AztecEditor-AndroidA reusable native Android rich text editor component.项目地址: https://gitcode.com/gh_mirrors/az/AztecEditor-Android

AztecEditor-Android是一个功能强大的原生Android富文本编辑器组件,专为HTML文档编辑而设计。这个开源项目基于EditText和Spannable API构建,提供了完整的富文本编辑体验。在本教程中,我将详细介绍如何自定义工具栏和实现Task List功能,帮助开发者快速上手这个优秀的Android富文本编辑器库。

🚀 快速开始:集成AztecEditor到你的项目

要使用AztecEditor,首先需要在你的build.gradle文件中添加依赖:

dependencies { implementation 'org.wordpress:aztec:latest-version' }

然后在布局文件中添加编辑器组件:

<org.wordpress.aztec.AztecText android:id="@+id/visualEditor" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical" /> <org.wordpress.aztec.toolbar.AztecToolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" />

在Activity中初始化编辑器:

val visualEditor = findViewById<AztecText>(R.id.visualEditor) val toolbar = findViewById<AztecToolbar>(R.id.toolbar) Aztec.with(visualEditor, toolbar, this) .setImageLoader(GlideImageLoader(this)) .setVideoThumbnailLoader(GlideVideoThumbnailLoader(this)) .build()

🛠️ 自定义工具栏配置详解

AztecEditor的工具栏系统非常灵活,支持完全自定义。工具栏的核心类是AztecToolbar,它提供了丰富的配置选项。

基本工具栏配置

工具栏按钮通过ToolbarAction枚举类定义,每个按钮都对应特定的文本格式:

// 启用特定格式按钮 toolbar.setToolbarItems( listOf( ToolbarAction.BOLD, ToolbarAction.ITALIC, ToolbarAction.UNDERLINE, ToolbarAction.LINK, ToolbarAction.UNORDERED_LIST, ToolbarAction.ORDERED_LIST, ToolbarAction.TASK_LIST // Task List按钮 ) ) // 设置工具栏点击监听器 toolbar.setToolbarListener(object : IAztecToolbarClickListener { override fun onToolbarFormatButtonClicked(format: ITextFormat, isChecked: Boolean) { // 处理格式按钮点击 visualEditor.toggleFormatting(format) } override fun onToolbarMediaButtonClicked() { // 处理媒体按钮点击 } })

高级工具栏定制

你可以创建完全自定义的工具栏布局:

// 1. 创建自定义布局 val customToolbarLayout = R.layout.custom_toolbar // 2. 绑定自定义按钮 toolbar.bindCustomLayout(customToolbarLayout) // 3. 添加自定义按钮点击处理 findViewById<Button>(R.id.custom_button).setOnClickListener { // 执行自定义操作 visualEditor.insertHtml("<custom-tag>内容</custom-tag>") } // 4. 动态显示/隐藏工具栏按钮 toolbar.showToolbarAction(ToolbarAction.TASK_LIST, true) // 显示Task List按钮 toolbar.showToolbarAction(ToolbarAction.CODE, false) // 隐藏代码按钮

工具栏状态管理

工具栏会自动根据光标位置更新按钮状态:

// 监听编辑器选择变化 visualEditor.setOnSelectionChangedListener { start, end -> val activeFormats = visualEditor.getActiveFormats() toolbar.updateToolbarState(activeFormats) } // 获取当前激活的格式 val activeFormats = visualEditor.getActiveFormats() if (activeFormats.contains(AztecTextFormat.FORMAT_TASK_LIST)) { // 当前在Task List中 }

📋 Task List功能深度解析

Task List是AztecEditor的一个重要功能,它允许用户创建可勾选的待办事项列表。让我们深入了解其实现原理。

Task List的核心组件

Task List功能由以下几个核心组件构成:

  1. AztecTaskListSpan- 任务列表的Span实现
  2. TaskListClickHandler- 处理点击事件
  3. ListFormatter- 格式化逻辑
  4. ToolbarAction.TASK_LIST- 工具栏按钮

Task List Span实现

Task List的核心是AztecTaskListSpan类,它负责渲染复选框和跟踪选中状态:

// 查看Task List Span源码 // aztec/src/main/kotlin/org/wordpress/aztec/spans/AztecTaskListSpan.kt class AztecTaskListSpan( override var nestingLevel: Int, override var attributes: AztecAttributes = AztecAttributes(), context: Context, var listStyle: BlockFormatter.ListStyle = BlockFormatter.ListStyle(0, 0, 0, 0, 0), var onRefresh: ((AztecTaskListSpan) -> Unit)? = null ) : AztecListSpan(nestingLevel, listStyle.verticalPadding) { override fun drawLeadingMargin(c: Canvas, p: Paint, x: Int, dir: Int, top: Int, baseline: Int, bottom: Int, text: CharSequence, start: Int, end: Int, first: Boolean, l: Layout) { // 绘制复选框 val d: Drawable? = contextRef.get()?.resources?.getDrawable(R.drawable.ic_checkbox, null) // ... 绘制逻辑 } fun canToggle(): Boolean { // 防止快速重复点击 val canToggle = !toggled toggled = canToggle return canToggle } }

点击事件处理

TaskListClickHandler负责处理用户点击复选框的操作:

// aztec/src/main/kotlin/org/wordpress/aztec/TaskListClickHandler.kt class TaskListClickHandler(val listStyle: BlockFormatter.ListStyle) { fun handleTaskListClick(text: Spannable, off: Int, x: Int, startMargin: Int): Boolean { // 获取点击的Task List Span val clickedList = text.getSpans(off, off, AztecTaskListSpan::class.java) .maxByOrNull { it.nestingLevel } ?: return false // 计算点击位置 val depthMultiplier = (clickedList.nestingLevel / 2) + 1 val effectiveMargin = listStyle.leadingMargin() * depthMultiplier // 检查是否点击在复选框区域内 if (x + startMargin > (effectiveMargin + AztecTaskListSpan.PADDING_SPACE)) return false // 切换选中状态 val clickedLines = text.getSpans(off, off, AztecListItemSpan::class.java) val clickedLine = clickedLines.find { val spanStart = text.getSpanStart(it) spanStart == off || (spanStart == off - 1 && text.getSpanEnd(it) == off) } if (clickedLine != null && clickedList.canToggle()) { clickedLine.toggleCheck() clickedList.refresh() return true } return false } }

启用Task List功能

要在编辑器中启用Task List功能,需要进行以下配置:

// 1. 在Aztec初始化时启用Task List Aztec.with(visualEditor, toolbar, this) .setImageLoader(GlideImageLoader(this)) .enableTaskList() // 启用Task List功能 .build() // 2. 配置Task List样式 val listStyle = BlockFormatter.ListStyle( leadingMargin = 50, // 左边距 indicatorMargin = 10, // 指示器边距 indicatorColor = Color.BLACK, // 指示器颜色 verticalPadding = 8, // 垂直内边距 horizontalPadding = 4 // 水平内边距 ) // 3. 设置点击处理器 visualEditor.setTaskListClickHandler(TaskListClickHandler(listStyle)) // 4. 确保工具栏包含Task List按钮 toolbar.setToolbarItems( listOf( // ... 其他按钮 ToolbarAction.TASK_LIST, ToolbarAction.UNORDERED_LIST, ToolbarAction.ORDERED_LIST ) )

🔧 实战:创建自定义Task List样式

让我们创建一个自定义样式的Task List,支持不同的复选框样式和颜色。

步骤1:创建自定义Task List Span

class CustomTaskListSpan( nestingLevel: Int, attributes: AztecAttributes = AztecAttributes(), context: Context, listStyle: BlockFormatter.ListStyle, val checkboxColor: Int = Color.BLUE, val checkedDrawableRes: Int = R.drawable.custom_checkbox_checked, val uncheckedDrawableRes: Int = R.drawable.custom_checkbox_unchecked ) : AztecTaskListSpan(nestingLevel, attributes, context, listStyle) { override fun drawLeadingMargin(c: Canvas, p: Paint, x: Int, dir: Int, top: Int, baseline: Int, bottom: Int, text: CharSequence, start: Int, end: Int, first: Boolean, l: Layout) { if (!first || contextRef.get() == null) return val spanStart = (text as Spanned).getSpanStart(this) val spanEnd = text.getSpanEnd(this) if (start !in spanStart..spanEnd || end !in spanStart..spanEnd) return val lineIndex = getIndexOfProcessedLine(text, end) ?: return // 使用自定义Drawable val drawableRes = if (isChecked(text, lineIndex)) { checkedDrawableRes } else { uncheckedDrawableRes } val d = contextRef.get()?.resources?.getDrawable(drawableRes, null) d?.setTint(checkboxColor) // 计算绘制位置 val drawableHeight = (0.8 * (p.fontMetrics.bottom - p.fontMetrics.top)) val markerStartPosition: Float = x + (listStyle.indicatorMargin * dir) * 1f d?.setBounds( (markerStartPosition - drawableHeight * 0.8).toInt().coerceAtLeast(0), (baseline - drawableHeight * 0.8).toInt(), (markerStartPosition + drawableHeight * 0.2).toInt(), (baseline + drawableHeight * 0.2).toInt() ) d?.draw(c) } }

步骤2:创建自定义Formatter

class CustomTaskListFormatter( private val editor: AztecText, private val checkboxColor: Int = Color.BLUE ) : IFormatter { override fun applyFormat(start: Int, end: Int) { val text = editor.text as SpannableStringBuilder // 创建自定义Task List Span val taskListSpan = CustomTaskListSpan( nestingLevel = calculateNestingLevel(text, start), context = editor.context, listStyle = BlockFormatter.ListStyle( leadingMargin = 60, indicatorMargin = 15, indicatorColor = checkboxColor, verticalPadding = 10, horizontalPadding = 5 ), checkboxColor = checkboxColor ) // 应用Span text.setSpan( taskListSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ) // 添加列表项 applyListItemFormat(text, start, end, taskListSpan.nestingLevel + 1) } private fun calculateNestingLevel(text: Spanned, position: Int): Int { // 计算嵌套层级逻辑 return 0 } private fun applyListItemFormat(text: SpannableStringBuilder, start: Int, end: Int, nestingLevel: Int) { // 应用列表项格式 } }

步骤3:集成到编辑器

// 注册自定义Formatter val customFormatter = CustomTaskListFormatter(visualEditor, Color.RED) visualEditor.registerFormatter(AztecTextFormat.FORMAT_TASK_LIST, customFormatter) // 或者替换默认的Task List处理 visualEditor.setTaskListFormatter(customFormatter)

🎯 高级功能:Task List与HTML转换

AztecEditor支持Task List与HTML之间的双向转换,这是其核心功能之一。

HTML到Task List的解析

当从HTML解析时,Aztec会自动识别<ul>标签的data-type="taskList"属性:

<!-- Task List的HTML表示 --> <ul>val html = visualEditor.toHtml() // 输出: // <ul>// 添加自定义属性 val taskListSpan = AztecTaskListSpan( nestingLevel = 0, attributes = AztecAttributes().apply { put("data-custom", "value") put("data-priority", "high") }, context = context, listStyle = listStyle ) // 生成的HTML会包含自定义属性 // <ul />

🔍 调试与问题排查

在使用AztecEditor时,可能会遇到一些常见问题,这里提供解决方案。

常见问题1:Task List点击无响应

// 检查是否正确设置了点击处理器 if (visualEditor.taskListClickHandler == null) { visualEditor.setTaskListClickHandler(TaskListClickHandler(listStyle)) } // 检查边距计算 val listStyle = BlockFormatter.ListStyle( leadingMargin = 50, // 确保这个值足够大 indicatorMargin = 10, indicatorColor = Color.BLACK, verticalPadding = 8, horizontalPadding = 4 )

常见问题2:Task List样式不正确

// 1. 检查Drawable资源 val drawable = context.resources.getDrawable(R.drawable.ic_checkbox, null) if (drawable == null) { // Drawable资源不存在 Log.e("Aztec", "Checkbox drawable not found") } // 2. 检查颜色配置 val typedArray = context.obtainStyledAttributes( intArrayOf(android.R.attr.colorControlNormal) ) val defaultColor = typedArray.getColor(0, Color.BLACK) typedArray.recycle() // 3. 调试绘制边界 override fun drawLeadingMargin(...) { Log.d("Aztec", "Drawing bounds: $left, $top, $right, $bottom") // ... 绘制逻辑 }

常见问题3:嵌套Task List问题

// 处理嵌套Task List fun handleNestedTaskList(text: Spannable, position: Int) { val taskListSpans = text.getSpans(position, position, AztecTaskListSpan::class.java) // 获取最内层的Task List val innermostSpan = taskListSpans.maxByOrNull { it.nestingLevel } // 根据嵌套层级调整边距 val nestingLevel = innermostSpan?.nestingLevel ?: 0 val adjustedMargin = listStyle.leadingMargin() * (nestingLevel + 1) // 处理点击事件 // ... }

📊 性能优化建议

1. Span管理优化

// 避免频繁创建Span对象 val spanCache = mutableMapOf<Int, AztecTaskListSpan>() fun getOrCreateTaskListSpan(nestingLevel: Int): AztecTaskListSpan { return spanCache.getOrPut(nestingLevel) { AztecTaskListSpan( nestingLevel = nestingLevel, context = context, listStyle = listStyle ) } }

2. 批量操作优化

// 批量应用Task List格式 fun applyTaskListToMultipleLines(editor: AztecText, lineIndices: List<Int>) { editor.beginBatchEdit() try { lineIndices.forEach { lineIndex -> val lineStart = editor.layout.getLineStart(lineIndex) val lineEnd = editor.layout.getLineEnd(lineIndex) editor.toggleFormatting(AztecTextFormat.FORMAT_TASK_LIST, lineStart, lineEnd) } } finally { editor.endBatchEdit() } }

3. 内存优化

// 使用WeakReference避免内存泄漏 class SafeTaskListSpan(context: Context) { private val contextRef = WeakReference(context) fun draw(canvas: Canvas) { val context = contextRef.get() if (context != null) { // 安全地使用context val drawable = context.resources.getDrawable(R.drawable.ic_checkbox, null) // ... 绘制逻辑 } } }

🎨 扩展功能:自定义Task List主题

你可以创建不同主题的Task List来满足不同的设计需求。

主题配置类

data class TaskListTheme( val checkboxColor: Int, val checkedDrawable: Int, val uncheckedDrawable: Int, val textColor: Int, val backgroundColor: Int, val priorityColors: Map<String, Int> = mapOf( "high" to Color.RED, "medium" to Color.YELLOW, "low" to Color.GREEN ) ) // 预定义主题 object TaskListThemes { val DEFAULT = TaskListTheme( checkboxColor = Color.BLACK, checkedDrawable = R.drawable.checkbox_default_checked, uncheckedDrawable = R.drawable.checkbox_default_unchecked, textColor = Color.BLACK, backgroundColor = Color.TRANSPARENT ) val DARK = TaskListTheme( checkboxColor = Color.WHITE, checkedDrawable = R.drawable.checkbox_dark_checked, uncheckedDrawable = R.drawable.checkbox_dark_unchecked, textColor = Color.WHITE, backgroundColor = Color.DKGRAY ) val COLORFUL = TaskListTheme( checkboxColor = Color.BLUE, checkedDrawable = R.drawable.checkbox_colorful_checked, uncheckedDrawable = R.drawable.checkbox_colorful_unchecked, textColor = Color.BLUE, backgroundColor = Color.LTGRAY ) }

动态主题切换

class ThemedTaskListSpan( nestingLevel: Int, attributes: AztecAttributes = AztecAttributes(), context: Context, listStyle: BlockFormatter.ListStyle, var theme: TaskListTheme = TaskListThemes.DEFAULT ) : AztecTaskListSpan(nestingLevel, attributes, context, listStyle) { fun setTheme(newTheme: TaskListTheme) { theme = newTheme refresh() // 触发重绘 } override fun drawLeadingMargin(c: Canvas, p: Paint, x: Int, dir: Int, top: Int, baseline: Int, bottom: Int, text: CharSequence, start: Int, end: Int, first: Boolean, l: Layout) { // 使用主题颜色 p.color = theme.checkboxColor // 根据优先级使用不同颜色 val priority = attributes.getValue("data-priority") val priorityColor = theme.priorityColors[priority] ?: theme.checkboxColor p.color = priorityColor // ... 绘制逻辑 } }

📱 实际应用场景

场景1:待办事项应用

class TodoListEditor : AppCompatActivity() { private lateinit var editor: AztecText private lateinit var toolbar: AztecToolbar override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_todo_editor) editor = findViewById(R.id.editor) toolbar = findViewById(R.id.toolbar) // 配置为待办事项专用编辑器 configureTodoEditor() // 加载示例待办事项 loadSampleTodos() } private fun configureTodoEditor() { Aztec.with(editor, toolbar, this) .enableTaskList() .setToolbarItems(listOf( ToolbarAction.TASK_LIST, ToolbarAction.BOLD, ToolbarAction.ITALIC, ToolbarAction.LINK )) .build() // 自定义Task List样式 val todoStyle = BlockFormatter.ListStyle( leadingMargin = 60, indicatorMargin = 15, indicatorColor = Color.BLUE, verticalPadding = 12, horizontalPadding = 8 ) editor.setTaskListClickHandler(TaskListClickHandler(todoStyle)) } private fun loadSampleTodos() { val sampleHtml = """ <ul>class ProjectTaskEditor : Fragment() { // ... 初始化代码 fun setupTaskPriorities() { // 为不同优先级的任务设置不同样式 val highPriorityStyle = BlockFormatter.ListStyle( leadingMargin = 60, indicatorMargin = 15, indicatorColor = Color.RED, // 高优先级用红色 verticalPadding = 12, horizontalPadding = 8 ) val mediumPriorityStyle = BlockFormatter.ListStyle( leadingMargin = 60, indicatorMargin = 15, indicatorColor = Color.YELLOW, // 中优先级用黄色 verticalPadding = 12, horizontalPadding = 8 ) val lowPriorityStyle = BlockFormatter.ListStyle( leadingMargin = 60, indicatorMargin = 15, indicatorColor = Color.GREEN, // 低优先级用绿色 verticalPadding = 12, horizontalPadding = 8 ) // 根据任务优先级应用不同样式 applyPriorityStyles() } fun exportToMarkdown(): String { val html = editor.toHtml() // 将HTML格式的Task List转换为Markdown return convertTaskListToMarkdown(html) } }

🔧 工具函数与扩展

扩展函数

// 为AztecText添加扩展函数 fun AztecText.toggleTaskListAtLine(line: Int) { val layout = this.layout val lineStart = layout.getLineStart(line) val lineEnd = layout.getLineEnd(line) this.toggleFormatting(AztecTextFormat.FORMAT_TASK_LIST, lineStart, lineEnd) } fun AztecText.getTaskListItems(): List<TaskListItem> { val text = this.text as SpannableString val taskListSpans = text.getSpans(0, text.length, AztecTaskListSpan::class.java) return taskListSpans.map { taskListSpan -> val start = text.getSpanStart(taskListSpan) val end = text.getSpanEnd(taskListSpan) val items = text.getSpans(start, end, AztecListItemSpan::class.java) TaskListItem( span = taskListSpan, items = items.map { itemSpan -> val itemStart = text.getSpanStart(itemSpan) val itemEnd = text.getSpanEnd(itemSpan) val isChecked = itemSpan.attributes.getValue("checked") == "true" val text = text.substring(itemStart, itemEnd) TaskItem(text, isChecked) } ) } } data class TaskListItem( val span: AztecTaskListSpan, val items: List<TaskItem> ) data class TaskItem( val text: String, val isChecked: Boolean )

工具类

object TaskListUtils { fun calculateCompletionPercentage(editor: AztecText): Float { val taskItems = editor.getTaskListItems() if (taskItems.isEmpty()) return 0f val totalItems = taskItems.sumOf { it.items.size } val completedItems = taskItems.sumOf { taskList -> taskList.items.count { it.isChecked } } return if (totalItems > 0) { completedItems.toFloat() / totalItems * 100 } else { 0f } } fun getTaskStatistics(editor: AztecText): TaskStatistics { val taskItems = editor.getTaskListItems() return TaskStatistics( totalTaskLists = taskItems.size, totalTasks = taskItems.sumOf { it.items.size }, completedTasks = taskItems.sumOf { taskList -> taskList.items.count { it.isChecked } }, completionPercentage = calculateCompletionPercentage(editor) ) } data class TaskStatistics( val totalTaskLists: Int, val totalTasks: Int, val completedTasks: Int, val completionPercentage: Float ) }

📚 学习资源与最佳实践

官方文档路径

  • 核心编辑器实现aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt
  • 工具栏系统aztec/src/main/kotlin/org/wordpress/aztec/toolbar/
  • Task List实现aztec/src/main/kotlin/org/wordpress/aztec/TaskListClickHandler.kt
  • Span定义aztec/src/main/kotlin/org/wordpress/aztec/spans/AztecTaskListSpan.kt
  • 格式化器aztec/src/main/kotlin/org/wordpress/aztec/formatting/ListFormatter.kt

最佳实践

  1. 性能优化:在处理大量Task List时,使用beginBatchEdit()endBatchEdit()包装批量操作
  2. 内存管理:及时清理不再使用的Span对象,避免内存泄漏
  3. 用户体验:为Task List点击提供视觉反馈,如涟漪效果
  4. 可访问性:为复选框添加内容描述,支持屏幕阅读器
  5. 错误处理:处理边界情况,如空列表、越界点击等

调试技巧

// 启用调试日志 fun enableDebugLogging() { // 添加Span变化监听 editor.addSpanWatcher(object : SpanWatcher { override fun onSpanAdded(text: Spannable?, what: Any?, start: Int, end: Int) { if (what is AztecTaskListSpan) { Log.d("AztecDebug", "TaskListSpan added: $start-$end, nesting: ${what.nestingLevel}") } } override fun onSpanRemoved(text: Spannable?, what: Any?, start: Int, end: Int) { // 处理Span移除 } override fun onSpanChanged(text: Spannable?, what: Any?, ostart: Int, oend: Int, nstart: Int, nend: Int) { // 处理Span变化 } }) // 跟踪点击事件 editor.setOnTouchListener { v, event -> if (event.action == MotionEvent.ACTION_DOWN) { val x = event.x.toInt() val y = event.y.toInt() val offset = editor.getOffsetForPosition(x, y) Log.d("AztecDebug", "Touch at ($x, $y), offset: $offset") } false } }

🎉 总结

通过本教程,你已经全面掌握了AztecEditor-Android中自定义工具栏和Task List功能的实现方法。AztecEditor提供了强大而灵活的API,让你能够创建功能丰富的富文本编辑器。

关键要点

  • AztecEditor基于Span系统,性能优秀且扩展性强
  • 工具栏完全可定制,支持自定义按钮和布局
  • Task List功能完善,支持嵌套、点击事件和HTML转换
  • 良好的架构设计,便于二次开发和功能扩展

无论是构建简单的笔记应用还是复杂的内容管理系统,AztecEditor都能提供强大的编辑能力。通过合理利用其插件系统和扩展机制,你可以创建出功能丰富、用户体验优秀的Android富文本编辑器。

现在就开始使用AztecEditor-Android,为你的Android应用添加专业的富文本编辑功能吧!

【免费下载链接】AztecEditor-AndroidA reusable native Android rich text editor component.项目地址: https://gitcode.com/gh_mirrors/az/AztecEditor-Android

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询