@
在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
//Android
var platformView = handler.PlatformView as AppCompatEditText;
//iOS
var platformView = handler.PlatformView as UITextView;
不同平臺的程式碼,通過.Net6的條件編譯實現,有關條件編譯的詳細資訊,請參考官方檔案。這次實現的是Android和iOS平臺,所以在程式碼中條件編譯語句如下
#if ANDROID
//android codes
...
#endif
#if IOS
//iOS codes
...
#endif
定義StyleType列舉,用於控制元件可以處理的文字樣式更改請求型別。
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<string> SetHtmlRequest;
public event EventHandler<StyleArgs> StyleChangeRequested;
public event EventHandler<SelectionArgs> SelectionChangeHandler;
建立StyleChangeRequested的訂閱事件以響應樣式變更請求,對應不同的樣式型別,呼叫不同的方法實現樣式變更。
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;
}
});
android端使用SelectionStart和SelectionEnd獲取選擇範圍,iOS端使用SelectedRange獲取選擇範圍
//Android
int getSelectionStart() => platformView.SelectionStart;
int getSelectionEnd() => platformView.SelectionEnd;
//iOS
NSRange 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互轉的對應實現。
在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<string>(
(sender, htmlString) =>
{
platformView.TextFormatted = HtmlParser_Android.HtmlToSpanned(htmlString);
}
);
在富文字編輯器中的內容,最終會生成一個帶有內聯樣式的HTML字串,如下所示:
控制元件由所見即所得編輯器和工具列組成,所見即所得編輯器用於顯示和編輯內容,工具列用於設定字號、顏色、加粗、斜體、下劃線
建立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>
工具列內的按鈕橫向排列
<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為字型顏色選擇器。
當點選字型大小選擇器時,彈出字型大小選擇器,當點選字型顏色選擇器時,彈出字型顏色選擇器。
<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>
後端程式碼,繫結一些預設值
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");
}
我在maui-sample專案中的一些控制元件,打算做成一個控制元件庫,方便大家使用。控制元件庫地址在下方。
maui-sample專案作為控制元件庫孵化器,程式碼可能會有點亂,也沒有經過嚴格的測試。當控制元件完善到一定程度,我會把控制元件封裝起來放到控制元件庫中。如果你有好的控制元件,歡迎pull request。
maui-sample:
Github:maui-samples
Mato.Maui控制元件庫
Mato.Maui
本文來自部落格園,作者:林曉lx,轉載請註明原文連結:https://www.cnblogs.com/jevonsflash/p/17473844.html