ideaki's blog

WinRT C#/XAML の開発について

WinRT C#/XAML お手軽に実装できる引っ張って更新コントロールの紹介

これは XAML Advent Calendar 2014 16日目の記事です。

@ideaki19です。WindowsストアをメインにC#/XAMLでアプリ開発を2年続けています。
2ちゃんねる専用ブラウザ sankaWindowsストアアプリで公開しています。
この記事ではsankaの一部機能を紹介します。
タイトルにがっつりネタバレしてありますが、引っ張って更新です!


いつかの記事で引っ張って更新を紹介しましたが...

WinRT C#/XAML Pull To Refresh Sample - ideaki's blog

実装がかなりめんどうです。引っ張りたいオブジェクトごとに長ったらしいXAMLとコードビハインドを書くのはいただけません。
記事内のXAMLが下

<ScrollViewer x:Name="scrollViewer"
                      HorizontalScrollMode="Disabled"
                      Loaded="scrollViewer_Loaded"
                      SizeChanged="scrollViewer_SizeChanged"
                      VerticalScrollBarVisibility="Hidden"
                      ViewChanged="scrollViewer_ViewChanged"
                      ZoomMode="Disabled">
    <StackPanel Orientation="Vertical">
        <Grid Height="60">
            <TextBlock x:Name="textBlock1"
                               HorizontalAlignment="Center"
                               VerticalAlignment="Top"
                               Opacity="0"
                               Text="refresh..." />
            <TextBlock x:Name="textBlock2"
                               HorizontalAlignment="Center"
                               VerticalAlignment="Bottom"
                               Opacity="0"
                               Text="pull to..." />
        </Grid>
        <ListView x:Name="listView"
                          Width="{Binding ActualWidth,
                                          ElementName=scrollViewer,
                                          Mode=OneWay}"
                          Height="{Binding ActualHeight,
                                           ElementName=scrollViewer,
                                           Mode=OneWay}"
                          ScrollViewer.VerticalScrollBarVisibility="Auto" />
    </StackPanel>
</ScrollViewer>

今回紹介するのが下

<ika:PullToRefreshPanel>
    <ListViewx:Name="listView"/>
</ika:PullToRefreshPanel>

すげー短い!
こんなに簡単なら実装しない手はないですね!
ContentControlを継承しているのでなんでも引っ張れます

デモ



ソースコードはこちら

ideaki/Ika.Controls · GitHub

以下作成手順です。
※コピペ注意報

  1. テンプレートコントロールの追加
  2. XAML(Style)の記述
  3. C#の記述

1. テンプレートコントロールの追加
プロジェクトをを右クリックし、新しい項目を選択します。
f:id:ideaki:20141215224422p:plain

クラス名をPullToRefreshPanel.csにして追加ボタンを押します。
f:id:ideaki:20141215224722p:plain

プロジェクトにはPullToRefreshPanel.csファイルのほかに、ThemesフォルダとGeneric.xamlファイルが追加されています。
Generic.xamlにはPullToRefreshPanel.csのXAMLを記述します。
f:id:ideaki:20141215225710p:plain

2. XAML(Style)の記述
PullToRefreshPanel.csはいったん置いといて、一緒に追加されたGeneric.xamlを開きましょう。
中身はResourceDictionaryですね、PullToRefreshPanel.csのStyleが記述されています。
不思議なことにこのResourceDictionaryをApp.xamlに追加しなくても、中のStyleを使用することができます(なんで?!)

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App11">

    <Style TargetType="local:PullToRefreshPanel">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:PullToRefreshPanel">
                    <Border
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Styleをざっくり書き換えてやりましょう。
中身はあの長ったらしいXAMLをTemplateにして記述しただけですが、ちょっとしたアニメーションを追加してます。
C#でアニメーションを記述するのはめんどうですがXAMLからだとらくちんです。
ちなみに下に書き換えてビルドすると失敗はしませんがエラーを吐きます、PullToRefreshPanel.csに存在しないプロパティを見に行ってるからです。
なのでちゃっちゃとC#を書きましょう!

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App11">
    <Style TargetType="local:PullToRefreshPanel">
        <Setter Property="FontSize" Value="24"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:PullToRefreshPanel">
                    <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}">

                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="VisualStates">
                                <VisualState x:Name="Normal" />
                                <VisualState x:Name="Pull">
                                    <Storyboard>
                                        <FadeInThemeAnimation TargetName="PullContent" />
                                        <FadeOutThemeAnimation TargetName="RefreshContent" />
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="Refresh">
                                    <Storyboard>
                                        <FadeInThemeAnimation TargetName="RefreshContent" />
                                        <FadeOutThemeAnimation TargetName="PullContent" />
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>

                        <ScrollViewer x:Name="ScrollViewer"
                                      HorizontalScrollMode="Disabled"
                                      VerticalScrollBarVisibility="Hidden"
                                      ZoomMode="Disabled">
                            <StackPanel x:Name="StackPanel">
                                <Grid Name="PullGrid"
                                      Width="{Binding Width,
                                                      ElementName=ScrollViewer}"
                                      Height="{TemplateBinding PullRange}">
                                    <ContentControl x:Name="RefreshContent"
                                                    HorizontalAlignment="Center"
                                                    VerticalAlignment="Bottom"
                                                    Content="{TemplateBinding RefreshContent}" />
                                    <ContentControl x:Name="PullContent"
                                                    HorizontalAlignment="Center"
                                                    VerticalAlignment="Bottom"
                                                    Content="{TemplateBinding PullContent}" />
                                </Grid>
                                <Grid x:Name="ContentGrid">
                                    <ContentPresenter />
                                </Grid>
                            </StackPanel>
                        </ScrollViewer>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

f:id:ideaki:20141215233807p:plain

3. C#の記述
PullToRefreshPanel.csを開きましょう
すでにコンストラクタに1行のおまじないが追加されています。

namespace App11
{
    public sealed class PullToRefreshPanel : Control
    {
        public PullToRefreshPanel()
        {
            this.DefaultStyleKey = typeof(PullToRefreshPanel);
        }
    }
}

下に書き換えます。
Styleやその中のTemplate、そして下のコードについて解説したいんですがいろいろと勉強不足なので...

namespace App11
{
    public class PullToRefreshPanel : ContentControl
    {
        bool isPullRefresh = false;

        public event EventHandler PullToRefresh;

        public PullToRefreshPanel()
        {
            DefaultStyleKey = typeof(PullToRefreshPanel);
        }

        public object RefreshContent
        {
            get { return (object)GetValue(RefreshContentProperty); }
            set { SetValue(RefreshContentProperty, value); }
        }

        public static readonly DependencyProperty RefreshContentProperty =
            DependencyProperty.Register("RefreshContent",
                                        typeof(object),
                                        typeof(PullToRefreshPanel),
                                        new PropertyMetadata("離して更新"));

        public object PullContent
        {
            get { return (object)GetValue(PullContentProperty); }
            set { SetValue(PullContentProperty, value); }
        }

        public static readonly DependencyProperty PullContentProperty =
            DependencyProperty.Register("PullContent",
                                        typeof(object),
                                        typeof(PullToRefreshPanel),
                                        new PropertyMetadata("引っ張って"));

        public double PullRange
        {
            get { return (double)GetValue(PullRangeProperty); }
            set { SetValue(PullRangeProperty, value); }
        }

        public static readonly DependencyProperty PullRangeProperty =
            DependencyProperty.Register("PullRange",
                                        typeof(double),
                                        typeof(PullToRefreshPanel),
                                        new PropertyMetadata(200.0));

        void UpdateView()
        {
            var grid = GetTemplateChild("PullGrid") as Grid;
            ScrollViewer.ChangeView(null, grid.ActualHeight, null);
            var contentgrid = GetTemplateChild("ContentGrid") as Grid;
            contentgrid.SetValue(HeightProperty, ScrollViewer.ActualHeight);
            contentgrid.SetValue(WidthProperty, ScrollViewer.ActualWidth);
            UpdateTransform();
        }

        void UpdateStates(bool useTransitions)
        {
            if (ScrollViewer.VerticalOffset == 0.0)
                VisualStateManager.GoToState(this, "Refresh", useTransitions);
            else
                VisualStateManager.GoToState(this, "Pull", useTransitions);
        }

        void UpdateTransform()
        {
            var element = GetTemplateChild("StackPanel") as UIElement;
            var transform = element.RenderTransform as CompositeTransform ?? new CompositeTransform();
            transform.TranslateY = (ScrollViewer.VerticalOffset - PullRange) * 0.8;
            element.RenderTransform = transform;
        }

        protected virtual void OnPullToRefresh(EventArgs e)
        {
            if (PullToRefresh != null)
                PullToRefresh(this, e);
        }

        protected override void OnApplyTemplate()
        {
            ScrollViewer = GetTemplateChild("ScrollViewer") as ScrollViewer;
            ScrollViewer.SizeChanged += (s, e) => UpdateView();
            ScrollViewer.ViewChanged += ScrollViewer_ViewChanged;
            this.Loaded += (s, e) => UpdateView();
            base.OnApplyTemplate();
        }

        void ScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
        {
            UpdateStates(true);
            UpdateTransform();

            if (ScrollViewer.VerticalOffset != 0.0)
                isPullRefresh = true;

            if (!e.IsIntermediate)
            {
                if (ScrollViewer.VerticalOffset == 0.0 && isPullRefresh)
                {
                    OnPullToRefresh(new EventArgs());

                    //await Task.Delay(50);
                }
                isPullRefresh = false;
                var grid = GetTemplateChild("PullGrid") as Grid;
                ScrollViewer.ChangeView(null, grid.ActualHeight, null);
            }
        }

        ScrollViewer scrollviewer;
        ScrollViewer ScrollViewer
        {
            get { return scrollviewer; }
            set { scrollviewer = value; }
        }
    }
}

出来上がりです。
試してみましょう。
MainPage.xamlを開いて下のXAMLを貼り付けます。

<Page x:Class="App11.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:local="using:App11"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <local:PullToRefreshPanel>
            <ListView>
                <ListViewItem Content="0" />
                <ListViewItem Content="1" />
                <ListViewItem Content="2" />
                <ListViewItem Content="3" />
                <ListViewItem Content="4" />
                <ListViewItem Content="5" />
                <ListViewItem Content="6" />
                <ListViewItem Content="7" />
                <ListViewItem Content="8" />
                <ListViewItem Content="9" />
            </ListView>
        </local:PullToRefreshPanel>
    </Grid>
</Page>

以上です。
明日は koty さんです。よろしくお願いします!