2011年12月5日月曜日

リストボックスを引っ張って更新

スマートフォンでSNS系のアプリを使っていて
「更新ボタン」を押しているとなんとなく未来感がない。
Twitterの公式アプリ等はそうだが、上側に引っ張るとリストが更新される。

WindowsPhoneでもこれを行なってみようと思ったら

高橋忍氏のブログや
その書籍プログラミングWindowsPhoneでも紹介されています。

んじゃ書く必要ないじゃん。と思いましたけど少し書いてみます。


スタイルを変更
ListBoxの中身はScrollViewerで構成されているので、 ScrollViewerのVerticalCompressionというVisualStateGroupを利用して ListBoxの状態を把握しようってことですね。 上記のブログにはPageごとの設定とありますけど 私的にはアプリケーション内で指定したいのでApp.xmlにApplication.Resoucesとして定義しています。
    <Application.Resources>
        <Style TargetType="ScrollViewer">
            <Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
            <Setter Property="HorizontalScrollBarVisibility" Value="Disabled"/>
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="Padding" Value="0"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="BorderBrush" Value="Transparent"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ScrollViewer">
                        <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}">
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="ScrollStates">
                                    <VisualStateGroup.Transitions>
                                        <VisualTransition GeneratedDuration="00:00:00.5"/>
                                    </VisualStateGroup.Transitions>
                                    <VisualState x:Name="Scrolling">
                                        <Storyboard>
                                            <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="VerticalScrollBar"/>
                                            <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="HorizontalScrollBar"/>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="NotScrolling"/>
                                </VisualStateGroup>
                                <VisualStateGroup x:Name="VerticalCompression">
                                    <VisualState x:Name="NoVerticalCompression"/>
                                    <VisualState x:Name="CompressionTop"/>
                                    <VisualState x:Name="CompressionBottom"/>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <Grid Margin="{TemplateBinding Padding}">
                                <ScrollContentPresenter x:Name="ScrollContentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}"/>
                                <ScrollBar x:Name="VerticalScrollBar" HorizontalAlignment="Right" Height="Auto" IsHitTestVisible="False" IsTabStop="False" Maximum="{TemplateBinding ScrollableHeight}" Minimum="0" Opacity="0" Orientation="Vertical" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" Value="{TemplateBinding VerticalOffset}" ViewportSize="{TemplateBinding ViewportHeight}" VerticalAlignment="Stretch" Width="5"/>
                                <ScrollBar x:Name="HorizontalScrollBar" HorizontalAlignment="Stretch" Height="5" IsHitTestVisible="False" IsTabStop="False" Maximum="{TemplateBinding ScrollableWidth}" Minimum="0" Opacity="0" Orientation="Horizontal" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" Value="{TemplateBinding HorizontalOffset}" ViewportSize="{TemplateBinding ViewportWidth}" VerticalAlignment="Bottom" Width="Auto"/>
                            </Grid>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Application.Resources>
この記述によって、アプリケーションのすべてのScrollViewerで圧縮した場合のイベントの発生が可能になります。 ※私には縦の必要がないのでVerticalのみです。
ListBoxから取り出してイベントを登録
上記ブログにある通り(そのままのコード)ListBoxからScrollViewerを取り出して 変更があったというイベントに登録します。
        private void ListBoxCompressionHandling(ListBox targetlistbox)
        {
            VisualStateGroup vgroup = new VisualStateGroup();

            // ListBox の初めに定義されている ScrollViewerを取り出す 
            ScrollViewer ListboxScrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(targetlistbox, 0);

            // Visual State はコントロールテンプレートの常に最上位に定義されている 
            FrameworkElement element = (FrameworkElement)VisualTreeHelper.GetChild(ListboxScrollViewer, 0);
            // Visual State を取り出しその中から 縦横Compression のVisualStateを取り出す 
            foreach (VisualStateGroup group in VisualStateManager.GetVisualStateGroups(element))
                if (group.Name == "VerticalCompression") vgroup = group;

            //縦横Compressionの状態が変わった時のイベントハンドラ 
            vgroup.CurrentStateChanging += new EventHandler<VisualStateChangedEventArgs>(ScrollViewer_CurrentStateChanging);
        } 
でイベントは
        void ScrollViewer_CurrentStateChanging(object sender, VisualStateChangedEventArgs e)
        {
            switch (e.NewState.Name) 
            { 
                case "CompressionTop":
                    break; 
                case "CompressionBottom": 
                    break;
                case "NoVerticalCompression":
                    break; 
                default: 
                    break; 
            } 
        }
で、この関数をLoadなどのイベントで呼び出すのですが Loadは何度も呼び出されるので、画面遷移が多いような画面だったら重複して登録してしまうので 一度だけ登録するようにしておくのが良いでしょう。
で何がしたいか?
これだと高橋忍氏のブログそのまま(アプリで登録した位の違い)です。 私的にはスクロールを「グッ」とした時にだけ、更新をしたいのです。 このままだと少しでも上にするだけでイベントが発生します。 んじゃ、圧縮イベントと圧縮が終わったイベントの時刻で処理してみよう!
アプローチ
        DateTime startTopTime = new DateTime(0);
        DateTime startBottomTime = new DateTime(0);
        DateTime endTime = new DateTime(0);
        private void InitCompression()
        {
            startTopTime = new DateTime(0);
            startBottomTime = new DateTime(0);
            endTime = new DateTime(0);
        }
        private Boolean IsCompressionTop() {
            if (!startTopTime.Equals(new DateTime(0)))
            {
                TimeSpan ts = endTime.Subtract(startTopTime);
                System.Diagnostics.Debug.WriteLine(ts.TotalSeconds);
                if (ts.TotalSeconds >= 0.9)
                {
                    return true;
                }
            }
            return false;
        }

        void ScrollViewer_CurrentStateChanging(object sender, VisualStateChangedEventArgs e)
        {
            switch (e.NewState.Name) 
            { 
                case "CompressionTop":
                    startTopTime = DateTime.Now;
                    break; 
                case "CompressionBottom": 
                    startBottomTime = DateTime.Now;
                    break;
                case "NoVerticalCompression":
                    endTime = DateTime.Now;
                    if (IsCompressionTop())
                    {
                        MessageBox.Show("できたよー");
                    }

                    InitCompression();
                    break; 
                default: 
                    break; 
            } 
        }
こうしてみると、確かにグッとした後にイベント発生ができるんだけど リストを離して戻った時の判定になってしまう。 できれば、グッとしている間にイベントを発生したい。 。。。もっといい方法があるような気がする。