当前位置: 首页 > 市场行情 >

[MAUI]写一个跨平台富文本编辑器

来源:博客园    时间:2023-06-12 05:36:26

@

目录原理创建编辑器定义实现复合样式选择范围字号字体颜色与背景色字体下划线字体加粗与斜体序列化和反序列化跨平台实现集成至编辑器创建控件使用控件最终效果已知问题项目地址富文本编辑器是一种所见即所得(what you see is what you get 简称 WYSIWYG)文本样式编辑器,用户在编辑器中输入内容和所做的样式修改,都会直接反映在编辑器中。

在Web端常见的有Quill、TinyMCE这些开源免费的富文本编辑器,而目前.NET MAUI方面没有类似的富文本编辑器可以免费使用。

使用.NET MAUI实现一个富文本编辑器并不难,今天就来写一个


(资料图)

使用.NET MAU实现跨平台支持,本项目可运行于Android、iOS平台。由于篇幅本文只展示Android平台的代码。

原理

.NET MAUI提供了编辑器控件,允许输入和编辑多行文本,虽然提供了字号,字体,颜色等控件属性,但我们无法为每个字符设置样式。我们将通过原生控件提供的范围选择器实现这一功能。

.NET MAUI提供了Handler的跨平台特性,我们将利用Handler实现所见即所得内容编辑器组件。这篇博文介绍了如何用Handler实现自定义跨平台控件,请阅读[MAUI程序设计] 用Handler实现自定义跨平台控件

在各平台中,我们将使用原生控件实现所见即所得的内容编辑器

Android使用SpannableString设置文本的复合样式,可以查看https://www.cnblogs.com/jisheng/archive/2013/01/10/2854088.html

*iOS使用NSAttributeString设置文本的复合样式,可以参考https://blog.csdn.net/weixin_44544690/article/details/124154949

创建编辑器

新建.NET MAUI项目,命名RichTextEditor

在Controls目录中创建WysiwygContentEditor,继承自Editor,用于实现所见即所得的内容编辑器

构造函数中注册HandlerChanged和HandlerChanging事件

public class WysiwygContentEditor : Editor{    public WysiwygContentEditor()    {        HandlerChanged+=WysiwygContentEditor_HandlerChanged;        HandlerChanging+=WysiwygContentEditor_HandlerChanging;    }}

在HandlerChanged事件中,获取Handler对象,通过它访问虚拟视图和本机视图。

private void WysiwygContentEditor_HandlerChanged(object sender, EventArgs e){    var handler = Handler;    if (handler != null)    {    }}

android端原生控件为AppCompatEditText,iOS端原生控件为UITextView

//Androidvar platformView = handler.PlatformView as AppCompatEditText;//iOSvar platformView = handler.PlatformView as UITextView;

不同平台的代码,通过.Net6的条件编译实现,有关条件编译的详细信息,请参考官方文档。这次实现的是Android和iOS平台,所以在代码中条件编译语句如下

#if ANDROID//android codes...#endif#if IOS//iOS codes...#endif
定义

定义StyleType枚举,用于控件可以处理的文本样式更改请求类型。

underline:字体下划线italic:字体斜体bold:字体加粗backgoundColor:字体背景色foregroundColor:字体前景色size:字体大小
public enum StyleType{    underline, italic, bold, backgoundColor, foregroundColor, size}

以及StyleArgs类,用于传递样式变更请求的参数

public class StyleArgs : EventArgs{    public StyleType Style;    public string Params;    public StyleArgs(StyleType style, string @params = null)    {        Style = style;        Params=@params;    }}

定义SelectionArgs类,用于传递选择范围变更请求的参数

public class SelectionArgs : EventArgs{    public int Start;    public int End;    public SelectionArgs(int start, int end)    {        Start = start;        End = end;    }}

定义事件用于各平台本机代码的调用

public event EventHandler GetHtmlRequest;public event EventHandler SetHtmlRequest;public event EventHandler StyleChangeRequested;public event EventHandler SelectionChangeHandler;

创建StyleChangeRequested的订阅事件以响应样式变更请求,对应不同的样式类型,调用不同的方法实现样式变更。

StyleChangeRequested =new EventHandler((sender, e) =>{    var EditableText = platformView.EditableText;    switch (e.Style)    {        case StyleType.underline:            UpdateUnderlineSpans(EditableText);            break;        case StyleType.italic:            UpdateStyleSpans(TypefaceStyle.Italic, EditableText);            break;        case StyleType.bold:            UpdateStyleSpans(TypefaceStyle.Bold, EditableText);            break;        case StyleType.backgoundColor:            UpdateBackgroundColorSpans(EditableText, Microsoft.Maui.Graphics.Color.FromArgb(e.Params));            break;        case StyleType.foregroundColor:            UpdateForegroundColorSpans(EditableText, Microsoft.Maui.Graphics.Color.FromArgb(e.Params));            break;        case StyleType.size:            UpdateAbsoluteSizeSpanSpans(EditableText, int.Parse(e.Params));            break;        default:            break;    }});
实现复合样式选择范围

android端使用SelectionStart和SelectionEnd获取选择范围,iOS端使用SelectedRange获取选择范围

//Androidint getSelectionStart() => platformView.SelectionStart;int getSelectionEnd() => platformView.SelectionEnd;//iOSNSRange getSelectionRange() => platformView.SelectedRange;
字号

MAUI控件中字号使用FontSize属性单位为逻辑像素,与DPI设置相关联。在android本机平台中,字号通过为EditableText对象设置AbsoluteSizeSpan实现,代码如下

void UpdateAbsoluteSizeSpanSpans(IEditable EditableText, int size){    var spanType = SpanTypes.InclusiveInclusive;    EditableText.SetSpan(new AbsoluteSizeSpan(size, true), getSelectionStart(), getSelectionEnd(), spanType);    SetEditableText(EditableText, platformView);}
字体颜色与背景色

Android平台中,字体颜色与背景色通过为EditableText对象设置ForegroundColorSpan和BackgroundColorSpan实现

void UpdateForegroundColorSpans(IEditable EditableText, Microsoft.Maui.Graphics.Color color){    var spanType = SpanTypes.InclusiveInclusive;    EditableText.SetSpan(new ForegroundColorSpan(color.ToAndroid()), getSelectionStart(), getSelectionEnd(), spanType);    SetEditableText(EditableText, platformView);}void UpdateBackgroundColorSpans(IEditable EditableText, Microsoft.Maui.Graphics.Color color){    var spanType = SpanTypes.InclusiveInclusive;    EditableText.SetSpan(new BackgroundColorSpan(color.ToAndroid()), getSelectionStart(), getSelectionEnd(), spanType);    SetEditableText(EditableText, platformView);}
字体下划线

将选择文本选择范围内若包含下划线,则移除下划线,否则添加下划线

Android平台中通过为EditableText对象设置UnderlineSpan实现为文本添加下划线,通过RemoveSpan方法可以移除下划线,

但选择范围可能已包含下划线片段的一部分,因此移除此下划线片段后,需要重新添加下划线片段,以实现部分移除的效果

void UpdateUnderlineSpans(IEditable EditableText){    var underlineSpans = EditableText.GetSpans(getSelectionStart(), getSelectionEnd(), Java.Lang.Class.FromType(typeof(UnderlineSpan)));    bool hasFlag = false;    var spanType = SpanTypes.InclusiveInclusive;    foreach (var span in underlineSpans)    {        hasFlag = true;        var spanStart = EditableText.GetSpanStart(span);        var spanEnd = EditableText.GetSpanEnd(span);        var newStart = spanStart;        var newEnd = spanEnd;        var startsBefore = false;        var endsAfter = false;        if (spanStart < getSelectionStart())        {            newStart = getSelectionStart();            startsBefore = true;        }        if (spanEnd > getSelectionEnd())        {            newEnd = getSelectionEnd();            endsAfter = true;        }        EditableText.RemoveSpan(span);        if (startsBefore)        {            EditableText.SetSpan(new UnderlineSpan(), spanStart, newStart, SpanTypes.ExclusiveExclusive);        }        if (endsAfter)        {            EditableText.SetSpan(new UnderlineSpan(), newEnd, spanEnd, SpanTypes.ExclusiveExclusive);        }    }    if (!hasFlag)    {        EditableText.SetSpan(new UnderlineSpan(), getSelectionStart(), getSelectionEnd(), spanType);    }    SetEditableText(EditableText, platformView);}
字体加粗与斜体

Android平台中,字体粗细与斜体通过为EditableText对象设置StyleSpan实现,与设置字体下划线一样,需要处理选择范围内已包含StyleSpan的情况

TypefaceStyle提供了Normal、Bold、Italic、BoldItalic四种字体样式,粗体+斜体样式是通过组合实现的,因此需要处理样式叠加问题

void UpdateStyleSpans(TypefaceStyle flagStyle, IEditable EditableText){    var styleSpans = EditableText.GetSpans(getSelectionStart(), getSelectionEnd(), Java.Lang.Class.FromType(typeof(StyleSpan)));    bool hasFlag = false;    var spanType = SpanTypes.InclusiveInclusive;    foreach (StyleSpan span in styleSpans)    {        var spanStart = EditableText.GetSpanStart(span);        var spanEnd = EditableText.GetSpanEnd(span);        var newStart = spanStart;        var newEnd = spanEnd;        var startsBefore = false;        var endsAfter = false;        if (spanStart < getSelectionStart())        {            newStart = getSelectionStart();            startsBefore = true;        }        if (spanEnd > getSelectionEnd())        {            newEnd = getSelectionEnd();            endsAfter = true;        }        if (span.Style == flagStyle)        {            hasFlag = true;            EditableText.RemoveSpan(span);            EditableText.SetSpan(new StyleSpan(TypefaceStyle.Normal), newStart, newEnd, spanType);        }        else if (span.Style == TypefaceStyle.BoldItalic)        {            hasFlag = true;            EditableText.RemoveSpan(span);            var flagLeft = TypefaceStyle.Bold;            if (flagStyle == TypefaceStyle.Bold)            {                flagLeft = TypefaceStyle.Italic;            }            EditableText.SetSpan(new StyleSpan(flagLeft), newStart, newEnd, spanType);        }        if (startsBefore)        {            EditableText.SetSpan(new StyleSpan(span.Style), spanStart, newStart, SpanTypes.ExclusiveExclusive);        }        if (endsAfter)        {            EditableText.SetSpan(new StyleSpan(span.Style), newEnd, spanEnd, SpanTypes.ExclusiveExclusive);        }    }    if (!hasFlag)    {        EditableText.SetSpan(new StyleSpan(flagStyle), getSelectionStart(), getSelectionEnd(), spanType);    }    SetEditableText(EditableText, platformView);}
序列化和反序列化

所见即所得的内容需要被序列化和反序列化以便存储或传输,我们仍然使用HTML作为中间语言,好在Android和iOS平台都有HTML互转的对应实现。

Android平台中,Android.Text.Html提供了FromHtml()和Html.ToHtml(),iOS中的NSAttributedStringDocumentAttributes提供了DocumentType属性,可以设置为NSHTMLTextDocumentType,使用它初始化AttributedString或调用AttributedString.GetDataFromRange()方法实现HTML和NSAttributedString的互转。跨平台实现

在Platform/Android目录下创建HtmlParser.Android作为Android平台序列化和反序列化的实现。

public static class HtmlParser_Android{    public static ISpanned HtmlToSpanned(string htmlString)    {        ISpanned spanned = Html.FromHtml(htmlString, FromHtmlOptions.ModeCompact);        return spanned;    }    public static string SpannedToHtml(ISpanned spanned)    {        string htmlString = Html.ToHtml(spanned, ToHtmlOptions.ParagraphLinesIndividual);        return htmlString;    }}

在Platform/iOS目录下创建HtmlParser.iOS作为iOS平台序列化和反序列化的实现。

public static class HtmlParser_iOS{    static nfloat defaultSize = UIFont.SystemFontSize;    static UIFont defaultFont;    public static NSAttributedString HtmlToAttributedString(string htmlString)    {        var nsString = new NSString(htmlString);        var data = nsString.Encode(NSStringEncoding.UTF8);        var dictionary = new NSAttributedStringDocumentAttributes();        dictionary.DocumentType = NSDocumentType.HTML;        NSError error = new NSError();        var attrString = new NSAttributedString(data, dictionary, ref error);        var mutString = ResetFontSize(new NSMutableAttributedString(attrString));        return mutString;    }    static NSAttributedString ResetFontSize(NSMutableAttributedString attrString)    {        defaultFont = UIFont.SystemFontOfSize(defaultSize);        attrString.EnumerateAttribute(UIStringAttributeKey.Font, new NSRange(0, attrString.Length), NSAttributedStringEnumeration.None, (NSObject value, NSRange range, ref bool stop) =>        {            if (value != null)            {                var oldFont = (UIFont)value;                var oldDescriptor = oldFont.FontDescriptor;                var newDescriptor = defaultFont.FontDescriptor;                bool hasBoldFlag = false;                bool hasItalicFlag = false;                if (oldDescriptor.SymbolicTraits.HasFlag(UIFontDescriptorSymbolicTraits.Bold))                {                    hasBoldFlag = true;                }                if (oldDescriptor.SymbolicTraits.HasFlag(UIFontDescriptorSymbolicTraits.Italic))                {                    hasItalicFlag = true;                }                if (hasBoldFlag && hasItalicFlag)                {                    uint traitsInt = (uint)UIFontDescriptorSymbolicTraits.Bold + (uint)UIFontDescriptorSymbolicTraits.Italic;                    newDescriptor = newDescriptor.CreateWithTraits((UIFontDescriptorSymbolicTraits)traitsInt);                }                else if (hasBoldFlag)                {                    newDescriptor = newDescriptor.CreateWithTraits(UIFontDescriptorSymbolicTraits.Bold);                }                else if (hasItalicFlag)                {                    newDescriptor = newDescriptor.CreateWithTraits(UIFontDescriptorSymbolicTraits.Italic);                }                var newFont = UIFont.FromDescriptor(newDescriptor, defaultSize);                attrString.RemoveAttribute(UIStringAttributeKey.Font, range);                attrString.AddAttribute(UIStringAttributeKey.Font, newFont, range);            }        });        return attrString;    }    public static string AttributedStringToHtml(NSAttributedString attributedString)    {        var range = new NSRange(0, attributedString.Length);        var dictionary = new NSAttributedStringDocumentAttributes();        dictionary.DocumentType = NSDocumentType.HTML;        NSError error = new NSError();        var data = attributedString.GetDataFromRange(range, dictionary, ref error);        var htmlString = new NSString(data, NSStringEncoding.UTF8);        return htmlString;    }}
集成至编辑器

在所见即所得编辑器中设置两个方法,一个用于获取编辑器中的内容,一个用于设置编辑器中的内容。

public void SetHtmlText(string htmlString){    HtmlString = htmlString;    SetHtmlRequest(this, htmlString);}public string GetHtmlText(){    GetHtmlRequest(this, new EventArgs());    return HtmlString;}

在HandlerChanged事件方法中的各平台代码段中添加如下代码:

GetHtmlRequest = new EventHandler(    (sender, e) =>        {            var editor = (WysiwygContentEditor)sender;            HtmlString=HtmlParser_Android.SpannedToHtml(platformView.EditableText);        }    );SetHtmlRequest =new EventHandler(    (sender, htmlString) =>        {            platformView.TextFormatted = HtmlParser_Android.HtmlToSpanned(htmlString);        }    );

在富文本编辑器中的内容,最终会生成一个带有内联样式的HTML字符串,如下所示:

创建控件

控件由所见即所得编辑器和工具栏组成,所见即所得编辑器用于显示和编辑内容,工具栏用于设置字号、颜色、加粗、斜体、下划线

创建RichTextEditor的带有Xaml的ContentView。将所见即所得编辑器放置中央,工具栏放置在底部。

                                                                                                                    

工具栏内的按钮横向排列

                    

配置两个选择器:TextSizeCollectionView为字体大小选择器,ColorCollectionView为字体颜色选择器。

当点击字体大小选择器时,弹出字体大小选择器,当点击字体颜色选择器时,弹出字体颜色选择器。

                                                                                                                                                                                                                                            

后端代码,绑定一些默认值

public static List DefaultTextColorList = new List() {    Color.FromArgb("#000000"),    Color.FromArgb("#F9371C"),    Color.FromArgb("#F97C1C"),    Color.FromArgb("#F9C81C"),    Color.FromArgb("#41D0B6"),    Color.FromArgb("#2CADF6"),    Color.FromArgb("#6562FC")};public static List DefaultTextSizeList = new List() {    new TextSize(){Name="Large", Value=22},    new TextSize(){Name="Middle", Value=18},    new TextSize(){Name="Small", Value=12},};

效果如下:

使用控件

在MainPage中使用RichTextEditor,代码如下

MainRichTextEditor.GetHtmlText()测试获取富文本编辑器Html序列化功能。

private async void Button_Clicked(object sender, EventArgs e){    var html = this.MainRichTextEditor.GetHtmlText();    await DisplayAlert("GetHtml()", html, "OK");}
最终效果已知问题HTML样式会重复添加项目地址

我在maui-sample项目中的一些控件,打算做成一个控件库,方便大家使用。控件库地址在下方。

maui-sample项目作为控件库孵化器,代码可能会有点乱,也没有经过严格的测试。当控件完善到一定程度,我会把控件封装起来放到控件库中。如果你有好的控件,欢迎pull request。

maui-sample:Github:maui-sample

Mato.Maui控件库Mato.Maui

上一篇:客户经理的工作总结

下一篇:最后一页

X 关闭

Copyright   2015-2022 北方空净网版权所有   备案号:京ICP备2021034106号-50   联系邮箱: 55 16 53 8@qq.com