开源的Android富文本编辑器

RichEditor

基于原生EditText+span实现的Android富文本编辑器
github地址:https://github.com/yuruiyin/RichEditor

组件描述

该组件是基于原生EditText+span的方式实现的,旨在提供一个功能齐全且使用方便的Android富文本编辑器。主要支持了加粗斜体等行内样式、标题引用等段内样式以及插入图片视频甚至自定义View等。

功能演示

Video_20190521_122847_513.gif

功能列表

  • 支持加粗、斜体、删除线、下划线行内样式
  • 支持插入标题、引用段内样式
  • 支持插入段落图片、视频
  • 支持插入段落自定义布局
  • 支持视频、gif和长图标记
  • 支持图片圆角
  • undo redo
  • [TODO] 支持行内ImageSpan,如类似微博@xxx,#话题名#
  • [TODO] 支持清除样式
  • [TODO] 编辑器内部复制粘贴ImageSpan(任意以ImageSpan方式插入的的类型,如图片、视频、自定义view等)

如何使用

gradle

Step 1. Add the JitPack repository in your root build.gradle at the end of repositories:

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

Step 2. Add the dependency in your app build.gradle:

dependencies {
    implementation 'com.github.yuruiyin:RichEditor:0.1.0'
}

参数定义

自定义属性名字 参数定义
editor_show_video_mark 是否显示视频标识图标
editor_video_mark_resource_id 视频图标资源id
editor_show_gif_mark 是否显示gif标识图标
editor_show_long_image_mark 是否显示长图标识
editor_image_radius 图片和视频圆角大小
editor_headline_text_size 标题字体大小

代码演示

说明:各个样式按钮的layout由调用方自行完成

1) 首先在xml中引用RichEditText:

<com.yuruiyin.richeditor.RichEditText
    android:id="@+id/richEditText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="20dp"
    android:background="#ffffff"
    android:gravity="top|left"
    android:hint="请输入..."
    android:inputType="textMultiLine"
    android:lineSpacingExtra="5dp"
    android:maxLength="20000"
    android:minHeight="350dp"
    android:paddingBottom="70dp"
    android:paddingLeft="15dp"
    android:paddingRight="15dp"
    android:paddingTop="23dp"
    android:textColor="#171717"
    android:textColorHint="#aaaaaa"
    android:textCursorDrawable="@null"
    android:textSize="16dp"
    app:editor_video_mark_resource_id="@mipmap/editor_video_mark_icon"
    app:editor_image_radius="3dp"
    app:editor_show_gif_mark="true"
    app:editor_show_video_mark="true"
    app:editor_show_long_image_mark="true"
    />

2) 针对加粗、斜体、标题等需要修改图标样式的按钮(不包括插入图片按钮),如加粗,处理如下:

    // 加粗
    richEditText.initStyleButton(
            StyleBtnVm(
                    RichTypeEnum.BOLD,
                    ivBold,
                    R.mipmap.icon_bold_normal,
                    R.mipmap.icon_bold_light
            )
    )

说明:其中ivBold为加粗ImageView,由调用方在layout中定义;R.mipmap.icon_bold_normal和R.mipmap.icon_bold_light是加粗按钮正常状态和点亮状态图片的资源id。

3)插入图片或视频

    /**
     * 处理插入图片
     */
    private fun handleAddImage() {
        val intent = Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
        startActivityForResult(intent, GET_PHOTO_REQUEST_CODE)
    }

    private fun doAddBlockImageSpan(
            realImagePath: String, blockImageSpanObtainObject: IBlockImageSpanObtainObject, isFromDraft: Boolean = false
    ) {
//        val blockImageSpanVm = BlockImageSpanVm(this, imageVm) // 不指定宽高,使用组件默认宽高
        val blockImageSpanVm =
                BlockImageSpanVm(blockImageSpanObtainObject, imageWidth, imageMaxHeight) // 指定宽高
        blockImageSpanVm.isFromDraft = isFromDraft
        richEditText.insertBlockImage(realImagePath, blockImageSpanVm) { blockImageSpan ->
            val spanObtainObject = blockImageSpan.blockImageSpanVm.spanObject
            when (spanObtainObject) {
                is ImageVm -> {
                    Toast.makeText(this, "短按了图片-当前图片路径:${spanObtainObject.path}", Toast.LENGTH_SHORT).show()
                }
                is VideoVm -> {
                    Toast.makeText(this, "短按了视频-当前视频路径:${spanObtainObject.path}", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == GET_PHOTO_REQUEST_CODE && resultCode == RESULT_OK && data != null) {
            // 相册图片返回
            val selectedImageUri = data.data ?: return
            val realImagePath = FileUtil.getFileRealPath(this, selectedImageUri) ?: return
            val fileType = FileUtil.getFileType(realImagePath) ?: return
            when (fileType) {
                FileTypeEnum.STATIC_IMAGE, FileTypeEnum.GIF -> {
                    val imageVm = ImageVm(realImagePath, "2")
                    doAddBlockImageSpan(realImagePath, imageVm)
                }
                FileTypeEnum.VIDEO -> {
                    // 插入视频封面
                    val videoVm = VideoVm(realImagePath, "3")
                    doAddBlockImageSpan(realImagePath, videoVm)
                }
            }
        }
    }
    

4) 插入自定义布局


    /**
     * 插入游戏
     */
    private fun handleAddGame() {
        val gameVm = GameVm(1, "一起来捉妖")
        doAddGame(gameVm)
    }

    private fun doAddGame(gameVm: GameVm, isFromDraft: Boolean = false) {
        val gameItemView = layoutInflater.inflate(R.layout.editor_game_item, null)
        val ivGameIcon = gameItemView.findViewById<ImageView>(R.id.ivGameIcon)
        val tvGameName = gameItemView.findViewById<TextView>(R.id.tvGameName)
        ivGameIcon.setImageResource(R.mipmap.icon_game_zhuoyao)
        tvGameName.text = gameVm.name

        ivGameIcon.layoutParams.width = gameIconSize
        ivGameIcon.layoutParams.height = gameIconSize

        val gameItemWidth = getEditTextWidthWithoutPadding()
        ViewUtil.layoutView(gameItemView, gameItemWidth, gameItemHeight)

        val blockImageSpanVm = BlockImageSpanVm(gameVm, gameItemWidth, imageMaxHeight)
        blockImageSpanVm.isFromDraft = isFromDraft
        richEditText.insertBlockImage(ViewUtil.getBitmap(gameItemView), blockImageSpanVm) { blockImageSpan ->
            val retGameVm = blockImageSpan.blockImageSpanVm.spanObject as GameVm
            // 点击游戏item
            Toast.makeText(this, "短按了游戏:${retGameVm.name}", Toast.LENGTH_SHORT).show()
        }
    }    

说明:插入自定义布局最终也是通过bitmap以ImageSpan的形式插入到编辑器中的。

5)获取数据

    // 返回的编辑器实体是一个list,list中每个元素代表一个段落block,具体block参数可以参考RichEditorBlock, 
    // 但是若需要保存草稿功能,则需要对该list进行转换成自己的实体,否则List<RichEditorBlock>序列化后反序列化会丢失数据,可以参考demo
    val conntent: List<RichEditorBlock> = richEditText.content

具体使用请参考demo

相关引用

  1. 设置EditText的光标高度: LineHeightEditText
  2. 设置图片圆角: RoundedImageView
  3. undo redo: AndroidEdit

最后

再次附上github地址:https://github.com/yuruiyin/RichEditor
欢迎star、fork、提issue~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容