しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

【Win10 Pre】 スクロール中にアプリバーを隠す UWP アプリを作る

前回Windows Phone 用に画面を分けました

f:id:matatabi_ux:20150720014209p:plain

さらに改良を加えて、スクロール中はアプリバーを隠すことで画面下部のアプリバー分だけコンテンツ表示領域を広く見せるようにしてみたいと思います

Windows 10 Insider Preview Build 10240 時点での情報のため、正式リリース後仕様等が変更になっている可能性があります

まずは GridView を拡張します

/// <summary>
/// Variable height tiles shows GridView
/// </summary>
public class VariableHeightGridView : GridView
{
    /// <summary>
    /// Internal ScrollViewer
    /// </summary>
    private ScrollViewer scrollViewer = null;

    /// <summary>
    /// Scrolling interval timer
    /// </summary>
    private DispatcherTimer timer = new DispatcherTimer();

    /// <summary>
    /// Dependency property whether scrolling is halted or not
    /// </summary>
    public static readonly DependencyProperty IsHaltedScrollingProperty = DependencyProperty.Register(
        "IsHaltedScrolling",
        typeof(bool),
        typeof(VariableHeightGridView),
        new PropertyMetadata(true));

    /// <summary>
    /// CLR property whether scrolling is halted or not
    /// </summary>
    public bool IsHaltedScrolling
    {
        get { return (bool)this.GetValue(IsHaltedScrollingProperty); }
        set { this.SetValue(IsHaltedScrollingProperty, value); }
    }

    /// <summary>
    /// Constructor
    /// </summary>
    public VariableHeightGridView()
    {
        this.SizeChanged += this.OnSizeChanged;
    }

    /// <summary>
    /// Size changed event handler
    /// </summary>
    /// <param name="sender">event sender</param>
    /// <param name="e">event arguments</param>
    private void OnSizeChanged(object sender, SizeChangedEventArgs e)
    {
        this.SizeChanged -= this.OnSizeChanged;

        this.scrollViewer = FindChild<ScrollViewer>(this);
        if (this.scrollViewer == null)
        {
            return;
        }

        this.scrollViewer.DirectManipulationStarted += this.OnDirectManipulationStarted;
        this.scrollViewer.DirectManipulationCompleted += this.OnDirectManipulationCompleted;

        this.timer.Interval = TimeSpan.FromSeconds(3);
        this.timer.Tick += this.OnTimerTick;
        this.timer.Start();
    }

    /// <summary>
    /// Internal ScrollViewer DirectManipulationStarted event handler
    /// </summary>
    /// <param name="sender">event sender</param>
    /// <param name="e">event arguments</param>
    private void OnDirectManipulationStarted(object sender, object e)
    {
        this.timer.Stop();

        this.IsHaltedScrolling = false;
    }

    /// <summary>
    /// Internal ScrollViewer DirectManipulationCompleted event handler
    /// </summary>
    /// <param name="sender">event sender</param>
    /// <param name="e">event arguments</param>
    private void OnDirectManipulationCompleted(object sender, object e)
    {
        this.IsHaltedScrolling = false;

        this.timer.Start();
    }

    /// <summary>
    /// Scrolling interval timer Tick event handler (3 seconds interval)
    /// </summary>
    /// <param name="sender">event sender</param>
    /// <param name="e">event arguments</param>
    private void OnTimerTick(object sender, object e)
    {
        this.timer.Stop();

        this.IsHaltedScrolling = true;
    }

    /// <summary>
    /// Unsubscribe events
    /// </summary>
    protected override void OnDisconnectVisualChildren()
    {
        if (this.timer != null)
        {
            this.timer.Stop();
            this.timer.Tick -= this.OnTimerTick;
        }
        if (this.scrollViewer != null)
        {
            this.scrollViewer.DirectManipulationStarted -= this.OnDirectManipulationStarted;
            this.scrollViewer.DirectManipulationCompleted -= this.OnDirectManipulationCompleted;
        }

        base.OnDisconnectVisualChildren();
    }

    /// <summary>
    /// Get a first visual element of specified type found visual tree children
    /// </summary>
    /// <typeparam name="T">target type</typeparam>
    /// <param name="root">visual tree root</param>
    /// <returns>found element</returns>
    public static T FindChild<T>(DependencyObject root, string name = null) where T : FrameworkElement
    {
        var result = root as T;
        if (result != null && (string.IsNullOrEmpty(name) || name.Equals(result.Name)))
        {
            return result;
        }

        int childCount = VisualTreeHelper.GetChildrenCount(root);
        for (int i = 0; i < childCount; i++)
        {
            var child = FindChild<T>(VisualTreeHelper.GetChild(root, i), name);
            if (child != null)
            {
                return child;
            }
        }
        return null;
    }

    ~ 中略 ~
}

Windows 10 から ScrollViewer にスクロール操作の開始と終了時のイベントとして追加された DirectManipulationStarted、DirectManipulationCompleted を利用しています

他にも結構いろいろやってますが、やりたいことは GridView 内部の DirectManipulationStarted イベント発生時に IsHaltedScrolling プロパティを false にし、DirectManipulationCompleted イベント発生から 3 秒間 DirectManipulationStarted イベントが起きない場合 true に戻す動きです

GridView 内部の ScrollViewer は通常参照できませんが VisualTreeHelper で内部ツリー構造をたどることで取得しています

次に ViewModel をちょっとだけ修正

/// <summary>
/// MainPage ViewModel
/// </summary>
public class MainPageViewModel : BindableBase
{
    /// <summary>
    /// Flag whther bottom bar is opened or not
    /// </summary>
    private bool isBottomBarOpen = true;

    /// <summary>
    /// Flag whther bottom bar is opened or not
    /// </summary>
    public bool IsBottomBarOpen
    {
        get { return this.isBottomBarOpen; }
        set { this.SetProperty<bool>(ref this.isBottomBarOpen, value); }
    }

    ~ 中略 ~
}

GridView からアプリバーにフラグの値を伝達するためのプロパティを追加しただけ

最後に View

<Page
    x:Class="SplitViewSample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:SplitViewSample"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:i="using:Microsoft.Xaml.Interactivity"
    xmlns:core="using:Microsoft.Xaml.Interactions.Core"
    RequestedTheme="Dark"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">        
        <local:VariableHeightGridView x:Name="GridView"
                                      Background="#222222"
                                      IsHaltedScrolling="{x:Bind ViewModel.IsBottomBarOpen, Mode=TwoWay, FallbackValue=True}"
                                      ScrollViewer.HorizontalScrollMode="Disabled"
                                      ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                                      ScrollViewer.VerticalScrollMode="Auto"
                                      ScrollViewer.VerticalScrollBarVisibility="Auto"
                                      ScrollViewer.ZoomMode="Disabled">
            <GridView.ItemsPanel>
                <ItemsPanelTemplate>
                    <VariableSizedWrapGrid Orientation="Horizontal"
                                           ItemWidth="180" 
                                           ItemHeight="1" 
                                           HorizontalAlignment="Center"/>
                </ItemsPanelTemplate>
            </GridView.ItemsPanel>
            <GridView.ItemTemplate>
                <DataTemplate>
                    <Border Padding="2,2,0,0">
                        <Image Source="{Binding ImageSource}" Stretch="Uniform"/>
                    </Border>
                </DataTemplate>
            </GridView.ItemTemplate>
        </local:VariableHeightGridView>
    </Grid>
    
    <Page.BottomAppBar>
        <CommandBar x:Name="BottomBar"
                    ClosedDisplayMode="Hidden"
                    IsOpen="{x:Bind ViewModel.IsBottomBarOpen, Mode=OneWay, FallbackValue=True}"
                    IsSticky="True">
            <CommandBar.PrimaryCommands>
                <AppBarButton Label="Search">
                    <AppBarButton.Icon>
                        <FontIcon FontFamily="Segoe MDL2 Assets" Glyph="&#xE094;"/>
                    </AppBarButton.Icon>
                </AppBarButton>
                <AppBarButton Label="Like">
                    <AppBarButton.Icon>
                        <FontIcon FontFamily="Segoe MDL2 Assets" Glyph="&#xE19F;"/>
                    </AppBarButton.Icon>
                </AppBarButton>
                <AppBarButton Label="Settings">
                    <AppBarButton.Icon>
                        <FontIcon FontFamily="Segoe MDL2 Assets" Glyph="&#xE115;"/>
                    </AppBarButton.Icon>
                </AppBarButton>
            </CommandBar.PrimaryCommands>
        </CommandBar>
    </Page.BottomAppBar>
</Page>

GridView の IsHaltedScrolling を ViewModel.IsBottomBarOpen プロパティ経由で CommandBar.IsOpen に伝わるようバインディングしました

CommandBar は閉じるときに隠れるよう ClosedDisplayMode="Hidden" を指定しています

これで実行

f:id:matatabi_ux:20150720114105g:plain

思い通りにできました