【Win10】頁面導航的實現


注:本文基於 Windows 10 10240 及其 SDK 編寫,若以后有變化,請以新版本為准。

 

頁面導航我們是再熟悉不過了,瀏覽器、手機 App 大多都使用這種方式來展示內容。在 Windows 10 應用商店應用當中,也是使用這種方式來展示內容。具體是通過 Frame 這個控件來進行導航展示。

在 App.xaml.cs 文件中,我們可以看到創建了一個 Frame:

QQ截圖20151125152030

並且在下面,使用 Navigate 方法導航到 App 的主頁 MainPage。

 

導航到某個頁面使用的就是 Navigate 方法,有三個重載,其實這個也沒什么好說的,看最復雜的,有三個參數的那個重載:

QQ截圖20151125152614

第一個參數是指需要導航到哪個頁面,第二個是需要傳遞的參數,第三個是指使用哪個過渡效果來導航到目標頁面。

一般我們用得最多的還是第二個和第一個重載。第三個是很少用的。

關於導航到某個頁面,就簡單說到這里,因為這並不是本文的重點。

 

接下來開始說本文的重點,導航的后退與前進,特別是后退。

如果說怎樣執行后退與前進的話,那代碼很簡單:

if (Frame.CanGoBack)
{
    Frame.GoBack();
}

if (Frame.CanGoForward)
{
    Frame.GoForward();
}

判斷好是否能后退/前進之后執行就可以了。另外也可以再加多一些條件判斷,例如是否正在登錄之類的,這些需要看具體的業務邏輯判斷。

 

但是,關鍵的問題來了,App 中你總得提供相應的 UI 入口來執行相應的功能吧。這就好比你寫了一個框架、類庫,總得有公開的方法給別人來調用。那么,在 Windows 10 應用商店應用當中,應該如何實現這個后退/前進的入口呢?接下來將探討一下。

 

一、設備上的后退鍵

一直做 Windows Phone Runtime App 開發的都會再熟悉不過了。我們是可以在 App 中捕捉到設備上的后退鍵被按下的。在 UWP 中,后退鍵被定義了在 Mobile Extensions 里,因此使用之前必須先添加引用:

QQ截圖20151125155057

但是,由於不是每台 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 時就立刻注冊一個按下邏輯:

QQ截圖20151125163029

那么如果當前頁面能夠后退的話,就后退,如果不能的話,那么什么也不做,讓后退鍵按下事件繼續傳遞。這種寫法代碼量比上面的通用的少很多,不需要在每個頁面導航進入和離開時訂閱/注銷后退鍵事件,但是沒辦法根據頁面邏輯來判斷是否能夠后退。因此各有優劣,大家可以視乎 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 中這么寫:

QQ截圖20151125165450

只要發生了導航就判斷是否能夠后退,從而是否顯示后退鍵。接下來訂閱事件和上面后退鍵的類似,不再細說。

 

三、鼠標側鍵前進和后退

如果你用的是 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 這個類也是同理,我們需要調用方法來提供數據,也就是把觸摸的點給它。

QQ截圖20151125193058

如果水平滑動發生的話,那么將會觸發 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 中你可以四種一起來用,也可以只用其中的一兩種,這個需要結合具體的業務需求來考慮。當然了,你也可以脫離這四種方式,例如利用加速度傳感器,向左甩機器就后退,向右甩就前進,至於實用性嘛,見仁見智了heia_thumb。最后,希望大家能夠有所得益,編寫出令用戶滿意的 App。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM