@
- 原理
-
創(chuàng)建編輯器
- 定義
-
實(shí)現(xiàn)復(fù)合樣式
- 選擇范圍
- 字號
- 字體顏色與背景色
- 字體下劃線
- 字體加粗與斜體
-
序列化和反序列化
- 跨平臺實(shí)現(xiàn)
- 集成至編輯器
- 創(chuàng)建控件
- 使用控件
- 最終效果
- 已知問題
- 項(xiàng)目地址
富文本編輯器是一種所見即所得(what you see is what you get 簡稱 WYSIWYG)文本編輯器,用戶在編輯器中輸入內(nèi)容和所做的樣式修改,都會直接反映在編輯器中。
在Web端常見的有Quill、TinyMCE這些開源免費(fèi)的富文本編輯器,而目前.NET MAUI方面沒有類似的富文本編輯器可以免費(fèi)使用。
使用.NET MAUI實(shí)現(xiàn)一個富文本編輯器并不難,今天就來寫一個
使用.NET MAU實(shí)現(xiàn)跨平臺支持,本項(xiàng)目可運(yùn)行于Android、iOS平臺。由于篇幅本文只展示Android平臺的代碼。
原理
.NET MAUI提供了編輯器控件,允許輸入和編輯多行文本,雖然提供了字號,字體,顏色等控件屬性,但我們無法為每個字符設(shè)置樣式。我們將通過原生控件提供的范圍選擇器實(shí)現(xiàn)這一功能。
.NET MAUI提供了Handler的跨平臺特性,我們將利用Handler實(shí)現(xiàn)所見即所得內(nèi)容編輯器組件。這篇博文介紹了如何用Handler實(shí)現(xiàn)自定義跨平臺控件,請閱讀[MAUI程序設(shè)計(jì)] 用Handler實(shí)現(xiàn)自定義跨平臺控件
在各平臺中,我們將使用原生控件實(shí)現(xiàn)所見即所得的內(nèi)容編輯器
-
Android使用SpannableString設(shè)置文本的復(fù)合樣式,可以查看https://www.cnblogs.com/jisheng/archive/2013/01/10/2854088.html
-
iOS使用NSAttributeString設(shè)置文本的復(fù)合樣式,可以參考https://blog.csdn.net/weixin_44544690/article/details/124154949
創(chuàng)建編輯器
新建.NET MAUI項(xiàng)目,命名RichTextEditor
在Controls目錄中創(chuàng)建WysiwygContentEditor,繼承自Editor,用于實(shí)現(xiàn)所見即所得的內(nèi)容編輯器
構(gòu)造函數(shù)中注冊HandlerChanged和HandlerChanging事件
public class WysiwygContentEditor : Editor
{
public WysiwygContentEditor()
{
HandlerChanged+=WysiwygContentEditor_HandlerChanged;
HandlerChanging+=WysiwygContentEditor_HandlerChanging;
}
}
在HandlerChanged事件中,獲取Handler對象,通過它訪問虛擬視圖和本機(jī)視圖。
private void WysiwygContentEditor_HandlerChanged(object sender, EventArgs e)
{
var handler = Handler;
if (handler != null)
{
}
}
android端原生控件為AppCompatEditText,iOS端原生控件為UITextView
//Android
var platformView = handler.PlatformView as AppCompatEditText;
//iOS
var platformView = handler.PlatformView as UITextView;
不同平臺的代碼,通過.Net6的條件編譯實(shí)現(xiàn),有關(guān)條件編譯的詳細(xì)信息,請參考官方文檔。這次實(shí)現(xiàn)的是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類,用于傳遞樣式變更請求的參數(shù)
public class StyleArgs : EventArgs
{
public StyleType Style;
public string Params;
public StyleArgs(StyleType style, string @params = null)
{
Style = style;
Params=@params;
}
}
定義SelectionArgs類,用于傳遞選擇范圍變更請求的參數(shù)
public class SelectionArgs : EventArgs
{
public int Start;
public int End;
public SelectionArgs(int start, int end)
{
Start = start;
End = end;
}
}
定義事件用于各平臺本機(jī)代碼的調(diào)用
public event EventHandler GetHtmlRequest;
public event EventHandler<string> SetHtmlRequest;
public event EventHandler<StyleArgs> StyleChangeRequested;
public event EventHandler<SelectionArgs> SelectionChangeHandler;
創(chuàng)建StyleChangeRequested的訂閱事件以響應(yīng)樣式變更請求,對應(yīng)不同的樣式類型,調(diào)用不同的方法實(shí)現(xiàn)樣式變更。
StyleChangeRequested =new EventHandler<StyleArgs>(
(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;
}
});
實(shí)現(xiàn)復(fù)合樣式
選擇范圍
android端使用SelectionStart和SelectionEnd獲取選擇范圍,iOS端使用SelectedRange獲取選擇范圍
//Android
int getSelectionStart() => platformView.SelectionStart;
int getSelectionEnd() => platformView.SelectionEnd;
//iOS
NSRange getSelectionRange() => platformView.SelectedRange;
字號
MAUI控件中字號使用FontSize屬性單位為邏輯像素,與DPI設(shè)置相關(guān)聯(lián)。
在android本機(jī)平臺中,字號通過為EditableText對象設(shè)置AbsoluteSizeSpan實(shí)現(xiàn),代碼如下
void UpdateAbsoluteSizeSpanSpans(IEditable EditableText, int size)
{
var spanType = SpanTypes.InclusiveInclusive;
EditableText.SetSpan(new AbsoluteSizeSpan(size, true), getSelectionStart(), getSelectionEnd(), spanType);
SetEditableText(EditableText, platformView);
}
字體顏色與背景色
Android平臺中,字體顏色與背景色通過為EditableText對象設(shè)置ForegroundColorSpan和BackgroundColorSpan實(shí)現(xiàn)
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);
}
字體下劃線
將選擇文本選擇范圍內(nèi)若包含下劃線,則移除下劃線,否則添加下劃線
Android平臺中通過為EditableText對象設(shè)置UnderlineSpan實(shí)現(xiàn)為文本添加下劃線,通過RemoveSpan方法可以移除下劃線,
但選擇范圍可能已包含下劃線片段的一部分,因此移除此下劃線片段后,需要重新添加下劃線片段,以實(shí)現(xiàn)部分移除的效果
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平臺中,字體粗細(xì)與斜體通過為EditableText對象設(shè)置StyleSpan實(shí)現(xiàn),與設(shè)置字體下劃線一樣,需要處理選擇范圍內(nèi)已包含StyleSpan的情況
TypefaceStyle提供了Normal、Bold、Italic、BoldItalic四種字體樣式,粗體+斜體樣式是通過組合實(shí)現(xiàn)的,因此需要處理樣式疊加問題
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);
}
序列化和反序列化
所見即所得的內(nèi)容需要被序列化和反序列化以便存儲或傳輸,我們?nèi)匀皇褂肏TML作為中間語言,好在Android和iOS平臺都有HTML互轉(zhuǎn)的對應(yīng)實(shí)現(xiàn)。
- Android平臺中,Android.Text.Html提供了FromHtml()和Html.ToHtml(),
- iOS中的NSAttributedStringDocumentAttributes提供了DocumentType屬性,可以設(shè)置為NSHTMLTextDocumentType,使用它初始化AttributedString或調(diào)用AttributedString.GetDataFromRange()方法實(shí)現(xiàn)HTML和NSAttributedString的互轉(zhuǎn)。
跨平臺實(shí)現(xiàn)
在Platform/Android目錄下創(chuàng)建HtmlParser.Android作為Android平臺序列化和反序列化的實(shí)現(xiàn)。
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目錄下創(chuàng)建HtmlParser.iOS作為iOS平臺序列化和反序列化的實(shí)現(xiàn)。
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;
}
}
集成至編輯器
在所見即所得編輯器中設(shè)置兩個方法,一個用于獲取編輯器中的內(nèi)容,一個用于設(shè)置編輯器中的內(nèi)容。
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<string>(
(sender, htmlString) =>
{
platformView.TextFormatted = HtmlParser_Android.HtmlToSpanned(htmlString);
}
);
在富文本編輯器中的內(nèi)容,最終會生成一個帶有內(nèi)聯(lián)樣式的HTML字符串,如下所示:
創(chuàng)建控件
控件由所見即所得編輯器和工具欄組成,所見即所得編輯器用于顯示和編輯內(nèi)容,工具欄用于設(shè)置字號、顏色、加粗、斜體、下劃線
創(chuàng)建RichTextEditor的帶有Xaml的ContentView。將所見即所得編輯器放置中央,工具欄放置在底部。
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:RichTextEditor.Controls;assembly=RichTextEditor"
x:Class="RichTextEditor.Controls.RichTextEditor">
<Border>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<controls:WysiwygContentEditor MinimumHeightRequest="150"
AutoSize="TextChanges"
BackgroundColor="{StaticResource PhoneContrastBackgroundBrush}"
IsSpellCheckEnabled="false"
x:Name="MainEditor"></controls:WysiwygContentEditor>
</Grid>
</Border>
</ContentView>
工具欄內(nèi)的按鈕橫向排列
<HorizontalStackLayout Grid.Row="3"
Spacing="5"
Margin="0,10">
<Button Text="{Binding Source={x:Reference TextSizeCollectionView}, Path=SelectedItem.Name, FallbackValue=Auto}"
Style="{StaticResource RichTextButtonStyle}"
Clicked="TextSizeButton_Clicked"
x:Name="TextSizeButton"></Button>
<Button Text="Color"
TextColor="{Binding Source={x:Reference ColorCollectionView}, Path=SelectedItem}"
Style="{StaticResource RichTextButtonStyle}"
Clicked="TextColorButton_Clicked"
x:Name="TextColorButton"></Button>
<Button Text="B"
Style="{StaticResource RichTextButtonStyle}"
FontAttributes="Bold"
x:Name="BoldButton"
Clicked="BoldButton_Clicked"></Button>
<Button Text="I"
Style="{StaticResource RichTextButtonStyle}"
FontAttributes="Italic"
x:Name="ItalicButton"
Clicked="ItalicButton_Clicked"></Button>
<Button Text="U"
Style="{StaticResource RichTextButtonStyle}"
FontAttributes="None"
x:Name="UnderLineButton"
Clicked="UnderLineButton_Clicked"></Button>
</HorizontalStackLayout>
配置兩個選擇器:TextSizeCollectionView為字體大小選擇器,ColorCollectionView為字體顏色選擇器。
當(dāng)點(diǎn)擊字體大小選擇器時,彈出字體大小選擇器,當(dāng)點(diǎn)擊字體顏色選擇器時,彈出字體顏色選擇器。
<VerticalStackLayout x:Name="OptionsLayout"
Grid.Row="2"
Spacing="5">
<CollectionView x:Name="TextSizeCollectionView"
Background="Transparent"
SelectionChanged="TextSizeCollectionView_SelectionChanged"
SelectionMode="Single"
HeightRequest="45">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Horizontal"
ItemSpacing="5"></LinearItemsLayout>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Border x:Name="TargetElement"
Style="{StaticResource SelectableLayoutStyle}"
Background="{StaticResource PhoneContrastBackgroundBrush}"
Padding="5,0">
<Label Text="{Binding Name}"
TextColor="{StaticResource PhoneForegroundBrush}"
VerticalOptions="Center"
FontSize="{Binding Value}"></Label>
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<CollectionView x:Name="ColorCollectionView"
SelectionChanged="ColorCollectionView_SelectionChanged"
SelectionMode="Single"
HeightRequest="45">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Horizontal"
ItemSpacing="5"></LinearItemsLayout>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Border x:Name="TargetElement"
Style="{StaticResource SelectableLayoutStyle}"
BackgroundColor="{Binding}"
WidthRequest="40"
HeightRequest="40"
StrokeShape="RoundRectangle 40">
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</VerticalStackLayout>
后端代碼,綁定一些默認(rèn)值
public static List<Color> DefaultTextColorList = new List<Color>() {
Color.FromArgb("#000000"),
Color.FromArgb("#F9371C"),
Color.FromArgb("#F97C1C"),
Color.FromArgb("#F9C81C"),
Color.FromArgb("#41D0B6"),
Color.FromArgb("#2CADF6"),
Color.FromArgb("#6562FC")
};
public static List<TextSize> DefaultTextSizeList = new List<TextSize>() {
new TextSize(){Name="Large", Value=22},
new TextSize(){Name="Middle", Value=18},
new TextSize(){Name="Small", Value=12},
};
效果如下:
使用控件
在MainPage中使用RichTextEditor,代碼如下
<controls:RichTextEditor
x:Name="MainRichTextEditor"
Text="{Binding Content}"
Placeholder="{Binding PlaceHolder}"></controls:RichTextEditor>
用MainRichTextEditor.GetHtmlText()
測試獲取富文本編輯器Html序列化功能。
private async void Button_Clicked(object sender, EventArgs e)
{
var html = this.MainRichTextEditor.GetHtmlText();
await DisplayAlert("GetHtml()", html, "OK");
}
最終效果
已知問題
- HTML樣式會重復(fù)添加
項(xiàng)目地址
我在maui-sample項(xiàng)目中的一些控件,打算做成一個控件庫,方便大家使用??丶斓刂吩谙路?。
maui-sample項(xiàng)目作為控件庫孵化器,代碼可能會有點(diǎn)亂,也沒有經(jīng)過嚴(yán)格的測試。當(dāng)控件完善到一定程度,我會把控件封裝起來放到控件庫中。如果你有好的控件,歡迎pull request。
maui-sample:
Github:maui-samples文章來源:http://www.zghlxwxcb.cn/news/detail-479369.html
Mato.Maui控件庫
Mato.Maui文章來源地址http://www.zghlxwxcb.cn/news/detail-479369.html
到了這里,關(guān)于[MAUI]寫一個跨平臺富文本編輯器的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!