注:本文基於 Windows 10 10240 及其 SDK 編寫,若以后有變化,請以新版本為准。
頁面導航我們是再熟悉不過了,瀏覽器、手機 App 大多都使用這種方式來展示內容。在 Windows 10 應用商店應用當中,也是使用這種方式來展示內容。具體是通過 Frame 這個控件來進行導航展示。
在 App.xaml.cs 文件中,我們可以看到創建了一個 Frame:
並且在下面,使用 Navigate 方法導航到 App 的主頁 MainPage。
導航到某個頁面使用的就是 Navigate 方法,有三個重載,其實這個也沒什么好說的,看最復雜的,有三個參數的那個重載:
第一個參數是指需要導航到哪個頁面,第二個是需要傳遞的參數,第三個是指使用哪個過渡效果來導航到目標頁面。
一般我們用得最多的還是第二個和第一個重載。第三個是很少用的。
關於導航到某個頁面,就簡單說到這里,因為這並不是本文的重點。
接下來開始說本文的重點,導航的后退與前進,特別是后退。
如果說怎樣執行后退與前進的話,那代碼很簡單:
if (Frame.CanGoBack) { Frame.GoBack(); } if (Frame.CanGoForward) { Frame.GoForward(); }
判斷好是否能后退/前進之后執行就可以了。另外也可以再加多一些條件判斷,例如是否正在登錄之類的,這些需要看具體的業務邏輯判斷。
但是,關鍵的問題來了,App 中你總得提供相應的 UI 入口來執行相應的功能吧。這就好比你寫了一個框架、類庫,總得有公開的方法給別人來調用。那么,在 Windows 10 應用商店應用當中,應該如何實現這個后退/前進的入口呢?接下來將探討一下。
一、設備上的后退鍵
一直做 Windows Phone Runtime App 開發的都會再熟悉不過了。我們是可以在 App 中捕捉到設備上的后退鍵被按下的。在 UWP 中,后退鍵被定義了在 Mobile Extensions 里,因此使用之前必須先添加引用:
但是,由於不是每台 Windows 10 設備上都存在后退鍵,因此必須先使用 ApiInformation 類做功能檢查。
protected override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); if (ApiInformation.IsEventPresent("Windows.Phone.UI.Input.HardwareButtons", "BackPressed")) { HardwareButtons.BackPressed += HardwareButtons_BackPressed; } } protected override void OnNavigatedFrom(NavigationEventArgs e) { base.OnNavigatedFrom(e); if (ApiInformation.IsEventPresent("Windows.Phone.UI.Input.HardwareButtons", "BackPressed")) { HardwareButtons.BackPressed -= HardwareButtons_BackPressed; } } private void HardwareButtons_BackPressed(object sender, BackPressedEventArgs e) { // 標記已處理該事件。 e.Handled = true; if (Frame.CanGoBack) { Frame.GoBack(); } }
這個是普遍最適用的寫法,在 HardwareButtons_BackPressed 方法中,我把 e.Handled 設置為 true,這是因為 BackPressed 是一個路由事件,如果不設置的話,其它注冊了事件處理方法也會收到通知,這是我們不希望的。然后我們可以判斷是否能夠后退,這里可以結合該頁面的特征來追加判斷邏輯,因此我說這是普遍最適用的寫法。頁面離開的話我們就注銷掉,因為每個頁面后退的邏輯並不一定相同。到新的頁面就再重新注冊。這種方式麻煩就在於每個頁面都要寫這么一段代碼,但是這是最通用的寫法。
如果我們的頁面后退總是不需要考慮當前頁面邏輯的話,我們可以在 App.xaml.cs 中,創建 Frame 時就立刻注冊一個按下邏輯:
那么如果當前頁面能夠后退的話,就后退,如果不能的話,那么什么也不做,讓后退鍵按下事件繼續傳遞。這種寫法代碼量比上面的通用的少很多,不需要在每個頁面導航進入和離開時訂閱/注銷后退鍵事件,但是沒辦法根據頁面邏輯來判斷是否能夠后退。因此各有優劣,大家可以視乎 App 的需求來進行選擇。
二、窗口左上角的后退鍵
但是在 Desktop 下,是沒有物理設備的后退鍵的,那么難道在 App 里面單獨做一個后退按鈕嗎?這是一個相當不優雅的解決方案。因此微軟在 Windows 10 中提供了一個新的 API,可以在窗口的左上角顯示一個后退按鈕。
SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = AppViewBackButtonVisibility.Visible;
調用這么一行代碼之后,我們的 App 的左上角就會看見一個后退按鈕。當然,這個按鈕只有在窗口模式下才顯示,mobile 端是看不見的。
那么我們的頁面代碼可以這么寫:
protected override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); SystemNavigationManager navigationManager = SystemNavigationManager.GetForCurrentView(); navigationManager.AppViewBackButtonVisibility = Frame.CanGoBack ? AppViewBackButtonVisibility.Visible : AppViewBackButtonVisibility.Collapsed; navigationManager.BackRequested += NavigationManager_BackRequested; } protected override void OnNavigatedFrom(NavigationEventArgs e) { base.OnNavigatedFrom(e); SystemNavigationManager.GetForCurrentView().BackRequested -= NavigationManager_BackRequested; } private void NavigationManager_BackRequested(object sender, BackRequestedEventArgs e) { e.Handled = true; if (Frame.CanGoBack) { Frame.GoBack(); } }
在進入到頁面的時候,我們先判斷當前頁面是否能后退,如果可以的話,就顯示,否則就不顯示。接下來訂閱事件,與設備后退鍵按下事件類似,我們需要將 e.Handled 設置為 true,以阻止事件繼續傳遞。然后后退之前,確保確實能后退,我們再后退。
如果類似設備后退鍵不需要考慮當前頁面邏輯的話,可以在 App.xaml.cs 中這么寫:
只要發生了導航就判斷是否能夠后退,從而是否顯示后退鍵。接下來訂閱事件和上面后退鍵的類似,不再細說。
三、鼠標側鍵前進和后退
如果你用的是 5 鍵鼠標的話,那么你會發現,使用瀏覽器的時候是能夠按那兩個側鍵來前進和后退的。那么,在 UWP 里又應該如何實現這種功能呢?
在 UWP 中,每個窗口都對應着一個 CoreWindow 對象,這個對象上能夠監聽許多輸入事件,包括鼠標按下和釋放。那么我們就可以編寫了:
protected override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); CoreWindow.GetForCurrentThread().PointerReleased += Page_PointerReleased; } protected override void OnNavigatedFrom(NavigationEventArgs e) { base.OnNavigatedFrom(e); CoreWindow.GetForCurrentThread().PointerReleased -= Page_PointerReleased; } private void Page_PointerReleased(CoreWindow sender, PointerEventArgs args) { args.Handled = true; switch (args.CurrentPoint.Properties.PointerUpdateKind) { case PointerUpdateKind.XButton1Released:// 鼠標后退鍵。 if (Frame.CanGoBack) { Frame.GoBack(); } break; case PointerUpdateKind.XButton2Released:// 鼠標前進鍵。 if (Frame.CanGoForward) { Frame.GoForward(); } break;
在頁面進入和頁面離開分別訂閱/注銷指針釋放事件。在 UWP 中,鼠標、手指觸摸、觸控筆這類輸入設備都歸類為 Pointer。
接下來,在事件訂閱方法中,從事件參數中先獲取當前點擊的點,然后再獲取相關信息,最后判斷是怎樣觸發的。
鼠標側鍵后退鍵叫 XButton1,前進鍵叫 XButton2。當然有的鼠標可能這兩個鍵是反轉的,但是由於我們這里獲取的是虛擬鍵,而不是實際鍵,因此並不需要進行特別處理。
在 App.xaml.cs 全局處理的代碼類似,這里就不再多說。
不過這種方法目前有一個缺點,就是在 WebView 控件中,這個事件是不會觸發的,因為 WebView 自身吞掉了這個事件,事件沒法冒泡到 CoreWindow 上。而且 JavaScript 沒有方法捕捉鼠標側鍵事件,因此不得不說這是一個遺憾。
四、觸摸手勢前進和后退
PS:2015 年 11 月 27 日修正了一下錯誤,具體請參見評論和 Demo,感謝 Youth.霖 發現。
不知道還在閱讀本文的你是否用過 WP 8.1 上的 IT 之家客戶端。它那個手勢滑動前進后退我覺得是做得相當不錯的。
要做這個效果,我們必須使用到手勢識別,但是這個邏輯如何來寫呢?難道得我們自己來寫判斷邏輯?答案是不需要的,在 UWP 中,我們使用 GestureRecognizer 類來做手勢識別。
所謂的手勢識別,無非就是計算一堆點,GestureRecognizer 這個類也是同理,我們需要調用方法來提供數據,也就是把觸摸的點給它。
如果水平滑動發生的話,那么將會觸發 CrossSliding 事件。
但是,GestureRecognizer 僅僅是告訴你滑動發生了,而從左往右滑還是從右往左滑這點是不會告訴你的。因此我們還需要記錄開始點和結束點的坐標來做判斷。改寫下代碼:
public sealed partial class MainPage : Page { private readonly GestureRecognizer _recognizer; private PointerPoint _startPoint; private PointerPoint _endPoint; public MainPage() { this.InitializeComponent(); _recognizer = new GestureRecognizer() { // 識別滑動手勢。 GestureSettings = GestureSettings.CrossSlide, // 識別水平滑動手勢。 CrossSlideHorizontally = true }; _recognizer.CrossSliding += GestureRecognizer_CrossSliding; } protected override void OnPointerPressed(PointerRoutedEventArgs e) { base.OnPointerPressed(e); PointerPoint point = e.GetCurrentPoint(null); _startPoint = point; _recognizer.ProcessDownEvent(point); } protected override void OnPointerMoved(PointerRoutedEventArgs e) { base.OnPointerMoved(e); _recognizer.ProcessMoveEvents(e.GetIntermediatePoints(null)); } protected override void OnPointerReleased(PointerRoutedEventArgs e) { base.OnPointerReleased(e); PointerPoint point = e.GetCurrentPoint(null); _endPoint = point; _recognizer.ProcessUpEvent(point); } private void GestureRecognizer_CrossSliding(GestureRecognizer sender, CrossSlidingEventArgs args) { if (args.CrossSlidingState == CrossSlidingState.Completed && (args.PointerDeviceType == PointerDeviceType.Pen || args.PointerDeviceType == PointerDeviceType.Touch))// 鼠標就不處理了。 { if (_startPoint != null && _endPoint != null)// 確保訪問 Position 不會異常。 { double startX = _startPoint.Position.X; double endX = _endPoint.Position.X; if (startX < endX) { // 從左往右,后退。 if (Frame.CanGoBack) { Frame.GoBack(); } } else if (startX > endX) { // 從右往左,前進。 if (Frame.CanGoForward) { Frame.GoForward(); } } } } }
那么這樣的話就能夠實現類似 IT 之家那種滑動前進和后退了。
如果是打算寫到 App.xaml.cs 的話,那么獲取點的方式需要稍微修改一下:
/// <summary> /// 提供特定於應用程序的行為,以補充默認的應用程序類。 /// </summary> sealed partial class App : Application { private readonly GestureRecognizer _recognizer; private PointerPoint _startPoint; private PointerPoint _endPoint; private CoreWindow _window; /// <summary> /// 初始化單一實例應用程序對象。這是執行的創作代碼的第一行, /// 已執行,邏輯上等同於 main() 或 WinMain()。 /// </summary> public App() { this.InitializeComponent(); this.Suspending += OnSuspending; _recognizer = new GestureRecognizer() { GestureSettings = GestureSettings.CrossSlide, CrossSlideHorizontally = true }; _recognizer.CrossSliding += GestureRecognizer_CrossSliding; } /// <summary> /// 在應用程序由最終用戶正常啟動時進行調用。 /// 將在啟動應用程序以打開特定文件等情況下使用。 /// </summary> /// <param name="e">有關啟動請求和過程的詳細信息。</param> protected override void OnLaunched(LaunchActivatedEventArgs e) { #if DEBUG if (System.Diagnostics.Debugger.IsAttached) { this.DebugSettings.EnableFrameRateCounter = true; } #endif Frame rootFrame = Window.Current.Content as Frame; // 不要在窗口已包含內容時重復應用程序初始化, // 只需確保窗口處於活動狀態 if (rootFrame == null) { // 創建要充當導航上下文的框架,並導航到第一頁 rootFrame = new Frame(); rootFrame.NavigationFailed += OnNavigationFailed; if (e.PreviousExecutionState == ApplicationExecutionState.Terminated) { //TODO: 從之前掛起的應用程序加載狀態 } // 將框架放在當前窗口中 Window.Current.Content = rootFrame; } if (rootFrame.Content == null) { // 當導航堆棧尚未還原時,導航到第一頁, // 並通過將所需信息作為導航參數傳入來配置 // 參數 rootFrame.Navigate(typeof(MainPage), e.Arguments); } // 確保當前窗口處於活動狀態 Window.Current.Activate(); if (_window == null) { _window = CoreWindow.GetForCurrentThread(); _window.PointerPressed += Window_PointerPressed; _window.PointerMoved += Window_PointerMoved; _window.PointerReleased += Window_PointerReleased; } } private void Window_PointerPressed(CoreWindow sender, PointerEventArgs args) { PointerPoint point = args.CurrentPoint; _startPoint = point; _recognizer.ProcessDownEvent(point); } private void Window_PointerMoved(CoreWindow sender, PointerEventArgs args) { _recognizer.ProcessMoveEvents(args.GetIntermediatePoints()); } private void Window_PointerReleased(CoreWindow sender, PointerEventArgs args) { PointerPoint point = args.CurrentPoint; _endPoint = point; _recognizer.ProcessUpEvent(point); } private void GestureRecognizer_CrossSliding(GestureRecognizer sender, CrossSlidingEventArgs args) { Frame rootFrame = Window.Current.Content as Frame; if (rootFrame == null) { return; } if (args.CrossSlidingState == CrossSlidingState.Completed && (args.PointerDeviceType == PointerDeviceType.Pen || args.PointerDeviceType == PointerDeviceType.Touch)) { if (_startPoint != null && _endPoint != null) { double startX = _startPoint.Position.X; double endX = _endPoint.Position.X; if (startX < endX) { if (rootFrame.CanGoBack) { rootFrame.GoBack(); } } else if (startX > endX) { if (rootFrame.CanGoForward) { rootFrame.GoForward(); } } } } }
改成從 CoreWindow 上獲取。而 CoreWindow 則在 Launched 方法最后初始化,因為 GetForCurrentThread 方法在 App 構造函數中調用是會返回 null 的。接下來剩下的代碼就差不多一樣了。
與第三點同樣的是,由於 WebView 吞掉事件的原因,這種方法在 WebView 里也是沒法觸發的。但是與鼠標側鍵不同的是,JavaScript 是能夠捕捉到主要輸入設備按下、移動、釋放這三個關鍵的事件的,因此我們可以使用 JavaScript 來捕捉 WebView 里面的手勢,然后通知 App,再執行前進或者后退。
這里我使用的 JS 手勢庫是 Hammer.js,官網是:http://hammerjs.github.io/
編寫 Html 模板:
<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" /> <title></title> <!--引用 HammerJS--> <script src="../Scripts/hammer.min.js" type="text/javascript"></script> </head> <body> 這里放內容。 <script type="text/javascript"> // 初始化監聽對象,並監聽 html。 var hammertime = new Hammer(document.getElementsByTagName("html")[0]); hammertime.on("swipeleft", function (e) { if (e.pointerType !== "mouse") { // 左滑,前進。 window.external.notify("goforward"); } }); hammertime.on("swiperight", function (e) { if (e.pointerType !== "mouse") { // 右滑,后退。 window.external.notify("goback"); } }); </script> </body> </html>
放內容這里可以通過 WebView.InvokeScriptAsync 之類的方法注入內容。而 JavaScript 通知 WebView 則需要使用 window.external.notify 方法,參數是一個字符串,調用后 WebView 的 ScriptNotify 事件將會觸發,並且會得到 JavaScript 傳遞過來的參數。在上面我們使用 e.PointerType 來做判斷輸入設備,同樣的,鼠標的話,不管它。
然后 cs 文件中獲取到 JavaScript 傳遞過來的參數就執行相應的前進后退。
private void WebView_ScriptNotify(object sender, NotifyEventArgs e) { string msg = e.Value; Debug.WriteLine(msg); switch (msg) { case "goforward": if (Frame.CanGoForward) { Frame.GoForward(); } break; case "goback": if (Frame.CanGoBack) { Frame.GoBack(); } break; } }
那么對於手勢前進、后退的話,WebView 也能夠得到支持了。
關於手勢前進、后退,具體可以參照這個渣渣的 Demo:GestureRecognizerNavigate_20151127.zip
鼠標用戶記得請使用模擬器來測試!!!
結語
以上四種方式算是目前比較常見的導航前進、后退的交互實現方式,當然在 App 中你可以四種一起來用,也可以只用其中的一兩種,這個需要結合具體的業務需求來考慮。當然了,你也可以脫離這四種方式,例如利用加速度傳感器,向左甩機器就后退,向右甩就前進,至於實用性嘛,見仁見智了。最后,希望大家能夠有所得益,編寫出令用戶滿意的 App。