@
- Handler
- 與Xamarin.Forms實現(xiàn)的區(qū)別
-
為什么要用Handler代替Renderer
- 解耦
- 生命周期管理
- 更細(xì)粒度的控制
- 用Effect來實現(xiàn)呢?
- 自定義手勢監(jiān)聽控件
-
在各平臺上實現(xiàn)TouchRecognizer
- iOS中的實現(xiàn)
- Android中的實現(xiàn)
- Windows中的實現(xiàn)
- 創(chuàng)建控件
- 使用控件
- 最終效果
- 項目地址
今天來談一談MAUI跨平臺技術(shù)的核心概念——跨平臺控件。
無論是MAUI,Xamarin.Forms還是其它的跨平臺技術(shù),他們是多個不同平臺功能的抽象層,利用通用的方法實現(xiàn)所謂“一次開發(fā),處處運行”。
跨平臺框架需要考慮通用方法在各平臺的兼容,但由于各原生平臺(官方將原生稱為本機)功能的差異,可能不能滿足特定平臺的所有功能。
比如,眾所周知,MAUI的手勢識別器沒有提供長按(LongPress)手勢的識別, TapGestureRecognizer也僅僅是按下和抬起的識別,沒有提供長按的識別。
這時候就需要開發(fā)者自己實現(xiàn)特定平臺的功能,這就是自定義控件。
要想重寫控件,或增強默認(rèn)控件的功能或視覺效果,最基礎(chǔ)的功能就是要拿到跨平臺控件,和本機控件。
通過跨平臺控件定義的屬性傳遞到本機控件,在本機控件中響應(yīng)和處理自定義屬性的變化。達(dá)到自定義控件的目的。
接下來介紹在MAUI新增的特性:控制器(Handler),好用但知道的人不多 。
Handler
因為跨平臺控件的實現(xiàn)由本機視圖在每個平臺上提供的,MAUI為每個控件創(chuàng)建了接口用于抽象控件。 實現(xiàn)這些接口的跨平臺控件稱為 虛擬視圖
。 處理程序 將這些虛擬視圖映射到每個平臺上的控件,這些控件稱為 本機視圖
。
在VisualElement中的Handler對象是一個實現(xiàn)了IElementHandler接口的類,通過它可以訪問 虛擬視圖
和 本機視圖
。
public interface IViewHandler : IElementHandler
{
bool HasContainer { get; set; }
object? ContainerView { get; }
IView? VirtualView { get; }
Size GetDesiredSize(double widthConstraint, double heightConstraint);
void PlatformArrange(Rect frame);
}
每個控件有各自的Handler以及接口,請查看官方文檔。
它可以通過注冊全局的映射器,作為特定本機平臺上實現(xiàn)自定義控件的功能的入口。
然后結(jié)合.NET 6 條件編譯的語言特性,可以更加方便在但文件上,為每個平臺編寫自定義處理程序。
Entry是實現(xiàn)IEntry接口的單行文本輸入控件,它對應(yīng)的Handler是EntryHandler。
如果我們想要在Entry控件獲取焦點時,自動全選文本。
Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping("MyCustomization", (handler, view) =>
{
#if ANDROID
handler.PlatformView.SetSelectAllOnFocus(true);
#elif IOS || MACCATALYST
handler.PlatformView.EditingDidBegin += (s, e) =>
{
handler.PlatformView.PerformSelector(new ObjCRuntime.Selector("selectAll"), null, 0.0f);
};
#elif WINDOWS
handler.PlatformView.GotFocus += (s, e) =>
{
handler.PlatformView.SelectAll();
};
#endif
});
或者,可以使用分部類將代碼組織到特定于平臺的文件夾和文件中。 有關(guān)條件編譯的詳細(xì)信息,請參考官方文檔。
與Xamarin.Forms實現(xiàn)的區(qū)別
在Xamarin.Forms時代,已經(jīng)提供了一套自定義控件的機制,呈現(xiàn)器(Renderer)。
Xamarin.Forms的控件,比如Entry是通過在封裝于特定平臺下的EntryRenderer的類中渲染的。
通過重寫控件默認(rèn)Renderer,可以完全改變控件的外觀和行為方式。
- Element,Xamarin.Forms 元素
- Control,本機視圖、小組件或控件對象
為什么要用Handler代替Renderer
雖然Renderer功能非常強大,但是絕大部分場景來說,不是每次都需要重寫控件,而僅僅是給控件添加一些特定平臺的增強功能,如果還需要重寫OnElementPropertyChanged 將跨平臺控件的屬性值傳輸?shù)奖緳C控件,這種方式太過于復(fù)雜。
以我的理解,Handler是對Renderer的一種優(yōu)化,它解決了Renderer的這些問題:Renderer和跨平臺控件的耦合,對自定義控件的生命周期管理,和對自定義控件的更細(xì)粒度控制。
解耦
在Xamarin.Froms的Render中,要想拿到跨平臺控件的屬性,需要通過直接引用跨平臺類型,這樣就導(dǎo)致了Renderer和跨平臺控件的耦合。
在MAUI中,處理程序會將平臺控件與框架分離。平臺控件只需處理框架的需求。這樣的好處是處理程序也適用于其他框架(如 Comet 和 Fabulous)重復(fù)使用。
生命周期管理
可以通過處理程序的映射器(Mapper)在應(yīng)用中的任意位置進(jìn)行處理程序自定義。 自定義處理程序后,它將影響在應(yīng)用中任意位置的該類型所有控件。
可以通過控件HandlerChanged 和HandlerChanging,管理Handler的生命周期,通過其參數(shù)可以獲取控件掛載、移除Handler的時機,可以在這里做一些初始化和清理工作。
更細(xì)粒度的控制
因為實現(xiàn)了全局映射器注冊,這樣的好處還有不用重寫子類控件,我們可以通過獲取跨平臺控件的某屬性,或注解屬性,拿到需要進(jìn)行處理的控件。實現(xiàn)自由的面向切面的過濾。
用Effect來實現(xiàn)呢?
或者我們僅僅想更改控件外觀,可以通過Effect來實現(xiàn)。但無論是Effect還是Renderer,他們只能是全局的,在需要狀態(tài)維護(hù)的業(yè)務(wù)邏輯中,比如長按,實際上是按下,抬起的過程,沒有按下的控件不要響應(yīng)抬起,正因為這樣要記錄哪些控件已經(jīng)按下,可能需要用一個字典維護(hù)所有的自定義控件。
而MAUI的自定義映射器實際上就是一個字典,減少了代碼的復(fù)雜度。
在MAUI中,官方建議遷移到Handler。Renderer雖仍然可以在MAUI中使用,但是它們屬于兼容方案(Compatibility命名空間),并且不提供ExportRenderer標(biāo)簽,需要在CreateMauiApp中手動添加:
.ConfigureMauiHandlers((handlers) =>
{
#if ANDROID
handlers.AddHandler(typeof(PressableView), typeof(XamarinCustomRenderer.Droid.Renderers.PressableViewRenderer));
#elif IOS
handlers.AddHandler(typeof(PressableView), typeof(XamarinCustomRenderer.iOS.Renderers.PressableViewRenderer));
#endif
});
從Renderer遷移到Handler的詳細(xì)步驟,請參考官方文檔
剛才說到,MAUI缺少長按的手勢控制,
所謂長按(LongPress),實際上是將手指接觸屏幕到離開屏幕的動作分解。當(dāng)手指接觸屏幕時,觸發(fā)按下(Pressed)事件,當(dāng)手指離開屏幕時,觸發(fā)抬起(Released)事件。如果在按下和抬起之間的時間間隔超過一定的時間,就認(rèn)為是長按。
對于這樣簡單的功能,MAUI團(tuán)隊并不打算將它加入到手勢識別中??赡軐⑦@個需求下放給社區(qū)來實現(xiàn),我在CommunityToolkit找到了這個issue(https://github.com/CommunityToolkit/Maui/issues/86)但是到目前為止,官方僅有的只是用Effect實現(xiàn)的手勢識別案例(https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/effects/touch-tracking)
那么我們參考這個官方案例,在MAUI上實現(xiàn)一個長按的手勢控制吧
自定義手勢監(jiān)聽控件
定義可以監(jiān)聽的手勢類別,分別是按下、移動、抬起、取消、進(jìn)入、退出
public enum TouchActionType
{
Entered,
Pressed,
Moved,
Released,
Exited,
Cancelled
}
添加手勢監(jiān)聽器TouchRecognizer,它將提供一個事件OnTouchActionInvoked,用觸發(fā)手勢動作。
public partial class TouchRecognizer: IDisposable
{
public event EventHandler<TouchActionEventArgs> OnTouchActionInvoked;
public partial void Dispose();
}
EventArg類TouchActionEventArgs,用于傳遞手勢動作的參數(shù)
public long Id { private set; get; }
public TouchActionType Type { private set; get; }
public Point Location { private set; get; }
public bool IsInContact { private set; get; }
在各平臺上實現(xiàn)TouchRecognizer
使用分布類(partial class)的方式,創(chuàng)建TouchRecognizer.iOS.cs
、TouchRecognizer.Android.cs
和TouchRecognizer.Windows.cs
文件,分別在各平臺上實現(xiàn)TouchRecognizer。在各平臺上的實現(xiàn)代碼不會混在一起,便于維護(hù)。
iOS中的實現(xiàn)
public partial class TouchRecognizer : UIGestureRecognizer, IDisposable
{
UIView iosView;
public TouchRecognizer(UIView view)
{
this.iosView = view;
}
public override void TouchesBegan(NSSet touches, UIEvent evt)
{
base.TouchesBegan(touches, evt);
foreach (UITouch touch in touches.Cast<UITouch>())
{
long id = touch.Handle.Handle.ToInt64();
InvokeTouchActionEvent(this, id, TouchActionType.Pressed, touch, true);
}
}
public override void TouchesMoved(NSSet touches, UIEvent evt)
{
base.TouchesMoved(touches, evt);
foreach (UITouch touch in touches.Cast<UITouch>())
{
long id = touch.Handle.Handle.ToInt64();
InvokeTouchActionEvent(this, id, TouchActionType.Moved, touch, true);
}
}
public override void TouchesEnded(NSSet touches, UIEvent evt)
{
base.TouchesEnded(touches, evt);
foreach (UITouch touch in touches.Cast<UITouch>())
{
long id = touch.Handle.Handle.ToInt64();
InvokeTouchActionEvent(this, id, TouchActionType.Released, touch, false);
}
}
public override void TouchesCancelled(NSSet touches, UIEvent evt)
{
base.TouchesCancelled(touches, evt);
foreach (UITouch touch in touches.Cast<UITouch>())
{
long id = touch.Handle.Handle.ToInt64();
InvokeTouchActionEvent(this, id, TouchActionType.Cancelled, touch, false);
}
}
void InvokeTouchActionEvent(TouchRecognizer recognizer, long id, TouchActionType actionType, UITouch touch, bool isInContact)
{
var cgPoint = touch.LocationInView(recognizer.View);
var xfPoint = new Point(cgPoint.X, cgPoint.Y);
OnTouchActionInvoked?.Invoke(this, new TouchActionEventArgs(id, actionType, xfPoint, isInContact));
}
}
Android中的實現(xiàn)
public partial class TouchRecognizer : IDisposable
{
Android.Views.View androidView;
Func<double, double> fromPixels;
int[] twoIntArray = new int[2];
private Point _oldscreenPointerCoords;
public TouchRecognizer(Android.Views.View view)
{
this.androidView = view;
if (view != null)
{
fromPixels = view.Context.FromPixels;
view.Touch += OnTouch;
}
}
public partial void Dispose()
{
androidView.Touch -= OnTouch;
}
void OnTouch(object sender, Android.Views.View.TouchEventArgs args)
{
var senderView = sender as Android.Views.View;
var motionEvent = args.Event;
var pointerIndex = motionEvent.ActionIndex;
var id = motionEvent.GetPointerId(pointerIndex);
senderView.GetLocationOnScreen(twoIntArray);
var screenPointerCoords = new Point(twoIntArray[0] + motionEvent.GetX(pointerIndex),
twoIntArray[1] + motionEvent.GetY(pointerIndex));
switch (args.Event.ActionMasked)
{
case MotionEventActions.Down:
case MotionEventActions.PointerDown:
InvokeTouchActionEvent(this, id, TouchActionType.Pressed, screenPointerCoords, true);
break;
case MotionEventActions.Move:
for (pointerIndex = 0; pointerIndex < motionEvent.PointerCount; pointerIndex++)
{
id = motionEvent.GetPointerId(pointerIndex);
senderView.GetLocationOnScreen(twoIntArray);
screenPointerCoords = new Point(twoIntArray[0] + motionEvent.GetX(pointerIndex),
twoIntArray[1] + motionEvent.GetY(pointerIndex));
if (IsOutPit(senderView, screenPointerCoords))
{
if (_oldscreenPointerCoords != default)
{
InvokeTouchActionEvent(this, id, TouchActionType.Exited, screenPointerCoords, true);
_oldscreenPointerCoords=default;
}
}
else
{
if (_oldscreenPointerCoords == default
||screenPointerCoords!= _oldscreenPointerCoords)
{
_oldscreenPointerCoords=screenPointerCoords;
InvokeTouchActionEvent(this, id, TouchActionType.Moved, screenPointerCoords, true);
}
}
}
break;
case MotionEventActions.Up:
case MotionEventActions.Pointer1Up:
InvokeTouchActionEvent(this, id, TouchActionType.Released, screenPointerCoords, false);
break;
case MotionEventActions.Cancel:
InvokeTouchActionEvent(this, id, TouchActionType.Cancelled, screenPointerCoords, false);
break;
}
}
private bool IsOutPit(Android.Views.View senderView, Point screenPointerCoords)
{
return (screenPointerCoords.X<twoIntArray[0]||screenPointerCoords.Y<twoIntArray[1])
||(screenPointerCoords.X>twoIntArray[0]+senderView.Width||screenPointerCoords.Y>twoIntArray[1]+senderView.Height);
}
void InvokeTouchActionEvent(TouchRecognizer touchEffect, int id, TouchActionType actionType, Point pointerLocation, bool isInContact)
{
touchEffect.androidView.GetLocationOnScreen(twoIntArray);
double x = pointerLocation.X - twoIntArray[0];
double y = pointerLocation.Y - twoIntArray[1];
var point = new Point(fromPixels(x), fromPixels(y));
OnTouchActionInvoked?.Invoke(this, new TouchActionEventArgs(id, actionType, point, isInContact));
}
}
Windows中的實現(xiàn)
public partial class TouchRecognizer : IDisposable
{
FrameworkElement windowsView;
public TouchRecognizer(FrameworkElement view)
{
this.windowsView = view;
if (this.windowsView != null)
{
this.windowsView.PointerEntered += View_PointerEntered;
this.windowsView.PointerPressed += View_PointerPressed;
this.windowsView.Tapped +=View_Tapped;
this.windowsView.PointerMoved += View_PointerMoved;
this.windowsView.PointerReleased += View_PointerReleased;
this.windowsView.PointerExited += View_PointerExited;
this.windowsView.PointerCanceled += View_PointerCancelled;
}
}
public partial void Dispose()
{
windowsView.PointerEntered -= View_PointerEntered;
windowsView.PointerPressed -= View_PointerPressed;
windowsView.Tapped -=View_Tapped;
windowsView.PointerMoved -= View_PointerMoved;
windowsView.PointerReleased -= View_PointerReleased;
windowsView.PointerExited -= View_PointerEntered;
windowsView.PointerCanceled -= View_PointerCancelled;
}
private void View_Tapped(object sender, TappedRoutedEventArgs args)
{
//var windowsPoint = args.GetPosition(sender as UIElement);
//Point point = new Point(windowsPoint.X, windowsPoint.Y);
//InvokeTouchActionEvent(TouchActionType.Pressed, point, 0, true);
}
private void View_PointerEntered(object sender, PointerRoutedEventArgs args)
{
Point point = GetPoint(sender, args);
var id = args.Pointer.PointerId;
var isInContact = args.Pointer.IsInContact;
InvokeTouchActionEvent(TouchActionType.Entered, point, id, isInContact);
}
private void View_PointerPressed(object sender, PointerRoutedEventArgs args)
{
Point point = GetPoint(sender, args);
var id = args.Pointer.PointerId;
var isInContact = args.Pointer.IsInContact;
InvokeTouchActionEvent(TouchActionType.Pressed, point, id, isInContact);
(sender as FrameworkElement).CapturePointer(args.Pointer);
}
private void View_PointerMoved(object sender, PointerRoutedEventArgs args)
{
Point point = GetPoint(sender, args);
var id = args.Pointer.PointerId;
var isInContact = args.Pointer.IsInContact;
InvokeTouchActionEvent(TouchActionType.Moved, point, id, isInContact);
}
private void View_PointerReleased(object sender, PointerRoutedEventArgs args)
{
Point point = GetPoint(sender, args);
var id = args.Pointer.PointerId;
var isInContact = args.Pointer.IsInContact;
InvokeTouchActionEvent(TouchActionType.Released, point, id, isInContact);
}
private void View_PointerExited(object sender, PointerRoutedEventArgs args)
{
Point point = GetPoint(sender, args);
var id = args.Pointer.PointerId;
var isInContact = args.Pointer.IsInContact;
InvokeTouchActionEvent(TouchActionType.Exited, point, id, isInContact);
}
private void View_PointerCancelled(object sender, PointerRoutedEventArgs args)
{
Point point = GetPoint(sender, args);
var id = args.Pointer.PointerId;
var isInContact = args.Pointer.IsInContact;
InvokeTouchActionEvent(TouchActionType.Cancelled, point, id, isInContact);
}
private void InvokeTouchActionEvent(TouchActionType touchActionType, Point point, uint id, bool isInContact)
{
OnTouchActionInvoked?.Invoke(this, new TouchActionEventArgs(id, touchActionType, point, isInContact));
}
private static Point GetPoint(object sender, PointerRoutedEventArgs args)
{
var pointerPoint = args.GetCurrentPoint(sender as UIElement);
Windows.Foundation.Point windowsPoint = pointerPoint.Position;
Point point = new Point(windowsPoint.X, windowsPoint.Y);
return point;
}
}
創(chuàng)建控件
創(chuàng)建手勢監(jiān)聽控件TouchContentView,它繼承于ContentView。
注意:盡量避免在構(gòu)造函數(shù)中調(diào)用ViewHandler.ViewMapper.AppendToMapping,它將導(dǎo)致從頁面的XAML根元素開始,遞歸遍歷所有IView虛擬視圖子元素,將其添加到ViewMapper中
我們用HandlerChanging監(jiān)聽Handler改變,當(dāng)OldHandler屬性不為空時,表示即將從跨平臺控件中刪除現(xiàn)有的本機控件,此時我們需要將TouchRecognizer移除,以免內(nèi)存泄漏。
public class TouchContentView : ContentView
{
private TouchRecognizer touchRecognizer;
public event EventHandler<TouchActionEventArgs> OnTouchActionInvoked;
public TouchContentView()
{
this.HandlerChanged+=TouchContentView_HandlerChanged;
this.HandlerChanging+=TouchContentView_HandlerChanging;
}
private void TouchContentView_HandlerChanged(object sender, EventArgs e)
{
var handler = this.Handler;
if (handler != null)
{
#if WINDOWS
touchRecognizer = new TouchRecognizer(handler.PlatformView as Microsoft.UI.Xaml.FrameworkElement);
touchRecognizer.OnTouchActionInvoked += TouchRecognizer_OnTouchActionInvoked;
#endif
#if ANDROID
touchRecognizer = new TouchRecognizer(handler.PlatformView as Android.Views.View);
touchRecognizer.OnTouchActionInvoked += TouchRecognizer_OnTouchActionInvoked;
#endif
#if IOS|| MACCATALYST
touchRecognizer = new TouchRecognizer(handler.PlatformView as UIKit.UIView);
touchRecognizer.OnTouchActionInvoked += TouchRecognizer_OnTouchActionInvoked;
(handler.PlatformView as UIKit.UIView).UserInteractionEnabled = true;
(handler.PlatformView as UIKit.UIView).AddGestureRecognizer(touchRecognizer);
#endif
}
}
private void TouchContentView_HandlerChanging(object sender, HandlerChangingEventArgs e)
{
if (e.OldHandler != null)
{
var handler = e.OldHandler;
#if WINDOWS
touchRecognizer.OnTouchActionInvoked -= TouchRecognizer_OnTouchActionInvoked;
#endif
#if ANDROID
touchRecognizer.OnTouchActionInvoked -= TouchRecognizer_OnTouchActionInvoked;
#endif
#if IOS|| MACCATALYST
touchRecognizer.OnTouchActionInvoked -= TouchRecognizer_OnTouchActionInvoked;
(handler.PlatformView as UIKit.UIView).UserInteractionEnabled = false;
(handler.PlatformView as UIKit.UIView).RemoveGestureRecognizer(touchRecognizer);
#endif
}
}
private void TouchRecognizer_OnTouchActionInvoked(object sender, TouchActionEventArgs e)
{
OnTouchActionInvoked?.Invoke(this, e);
Debug.WriteLine(e.Type + " is Invoked, position:" + e.Location);
}
}
使用控件
在Xaml中引用TouchContentView所在的命名空間
xmlns:controls="clr-namespace:Lession2.TouchRecognizer;assembly=Lession2"
將你的控件放在TouchContentView中,然后監(jiān)聽TouchContentView的OnTouchActionInvoked事件即可。
注意:對于Button這樣的點擊控件,點擊事件不會向下傳遞,因此如果包裹了Button,那么OnTouchActionInvoked事件將不會被觸發(fā)。
<controls:TouchContentView Style="{StaticResource HoldDownButtonStyle}"
Grid.Column="0"
OnTouchActionInvoked="TouchContentView_OnTouchActionInvoked">
<BoxView CornerRadius="10" Color="Red"></BoxView>
</controls:TouchContentView>
<controls:TouchContentView Style="{StaticResource HoldDownButtonStyle}"
Grid.Column="1"
OnTouchActionInvoked="TouchContentView_OnTouchActionInvoked">
<Image Source="./dotnet_bot.svg"></Image>
</controls:TouchContentView>
<controls:TouchContentView Style="{StaticResource HoldDownButtonStyle}"
Grid.Column="2"
OnTouchActionInvoked="TouchContentView_OnTouchActionInvoked">
<Label Text="假裝我是一個按鈕"></Label>
</controls:TouchContentView>
最終效果
在控件中將應(yīng)用手勢監(jiān)聽。
文章來源:http://www.zghlxwxcb.cn/news/detail-470935.html
項目地址
Github:maui-learning文章來源地址http://www.zghlxwxcb.cn/news/detail-470935.html
到了這里,關(guān)于[MAUI程序設(shè)計] 用Handler實現(xiàn)自定義跨平臺控件的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!