Статьи

Rowi-подобное меню «Show-On-Tap» для Windows Phone

Как Rowi, отличный клиент для Twitter для Windows Phone, выпустил очередное обновление, он представил очень интересный тип меню — «show-on-tap». Он отображается всякий раз, когда пользователь нажимает на один твит в любом месте приложения. Это в основном список опций, которые привязаны к ItemTemplate в ListBox , где элементы агрегированного. Вы можете увидеть, как это выглядит на скриншотах ниже:

Я думал, что для одного из приложений, над которыми я работаю, такая концепция будет работать намного лучше, чем меню всплывающих подсказок, которое разработчики привыкли использовать прямо сейчас. Основным недостатком, связанным с использованием традиционной всплывающей подсказки, является тот факт, что пользователь не знает, доступна ли эта опция или нет — требуется время реакции для отображения элемента управления. 

С другой стороны, меню «показ на касании» обеспечивает мгновенную обратную связь — как только пользователь нажимает на элемент, связанные действия мгновенно отображаются. Вот как я решил реализовать свой собственный элемент управления, который будет делать именно это — TapMenu . Думая о его структуре, легко выделить несколько основных компонентов, которые должны быть его частью:

  • ItemsPresenter с горизонтальным ScrollViewer — таким образом, элементы располагаются последовательно в горизонтальном пространстве.
  • TapMenuButton экземпляры внутри ListBox — это отдельные квадратные кнопки с настраиваемой DataTemplate для размещения в изображение и TextBlock для подписи.
  • Полупрозрачная сетка — используется для хранения ListBox и все еще обеспечивает визуальное представление содержимого позади него.

Я создал новый проект библиотеки классов для Windows Phone и назвал его ControlKit. В конечном итоге это станет моей собственной коллекцией элементов управления, которые я создал, так что, возможно, и название будет простым, но описательным. Я добавил класс с именем TapMenu, и вот что в нем:

using System.Windows.Controls;

namespace ControlKit
{
    public class TapMenu : ItemsControl
    {
        public TapMenu()
        {
            DefaultStyleKey = typeof(TapMenu);
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
        }
    }
}

Ничего особенного — он унаследован от ItemsControl, поэтому я могу легко отобразить множество элементов внутри него. Большая часть декларации управления происходит в файле темы, и я буду говорить об этом позже. TapMenu будет содержать несколько экземпляров TapMenuButton — те, которые должны содержать изображение и подпись. Вот как выглядит класс TapMenuButton:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace ControlKit
{
    public class TapMenuButton : Button
    {
        public static readonly DependencyProperty TextProperty =
          DependencyProperty.Register("Text", typeof(string), typeof(TapMenuButton), new PropertyMetadata(string.Empty));

        public static readonly DependencyProperty ImageSourceProperty =
          DependencyProperty.Register("ImageSource", typeof(ImageSource), typeof(TapMenuButton), null);

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

        public ImageSource ImageSource
        {
            get { return GetValue(ImageSourceProperty) as ImageSource; }
            set { SetValue(ImageSourceProperty, value); }
        }

        public string Text
        {
            get { return GetValue(TextProperty) as string; }
            set { SetValue(TextProperty, value); }
        }
    }
}

Теперь вам нужно установить визуальный стиль элемента управления. Это делается в файле Generic.xaml — создайте новую папку в проекте и назовите ее Themes . Затем добавьте новый файл и назовите его Generic.xaml .

ПРИМЕЧАНИЕ. Именование важно: текущее соглашение является требованием Silverlight и должно соблюдаться, чтобы элементы управления отображались правильно. 

Вот что у меня там:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows"
    xmlns:local="clr-namespace:ControlKit">
    <Style TargetType="local:TapMenu">
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <StackPanel Opacity=".8" Orientation="Horizontal"/>
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>

        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:TapMenu">
                    <ScrollViewer VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Hidden"  Height="150">
                         <ItemsPresenter></ItemsPresenter>
                    </ScrollViewer>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <Style TargetType="local:TapMenuButton">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:TapMenuButton">
                    <Button Margin="0,0,-15,0" Height="120" Width="120" Background="{StaticResource PhoneAccentBrush}" BorderThickness="0" Padding="0">
                        <StackPanel>
                            <Image Margin="0,12,0,12" HorizontalAlignment="Center" Height="36" Width="36" Stretch="UniformToFill" Source="{TemplateBinding ImageSource}"></Image>
                            <TextBlock Style="{StaticResource PhoneTextSmallStyle}" HorizontalAlignment="Center" Text="{TemplateBinding Text}"></TextBlock>
                        </StackPanel>
                    </Button>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Есть несколько вещей, которые стоит упомянуть:

  • Обратите внимание, что для StackPanel , представляющей ItemsPanelTemplate , для свойства Orientation установлено значение Horizontal . Это гарантирует, что элементы, которые будут использоваться в элементе управления, будут размещены в горизонтальной плоскости.
  • Для основного шаблона TapMenu обратите внимание, что в ScrollViewer отключена вертикальная прокрутка, а горизонтальная полоса прокрутки скрыта.
  • Для TapMenuButton и Image, и TextBlock используют TemplateBinding . Не делайте ошибку и используйте Binding — свойство, которое вам нужно прочитать, является частью основного шаблона.
  • TapMenuButton — это безграничная реализация простого элемента управления Button.
  • Цветная кисть фона для TapMenuButton определяется текущим цветом акцента, установленным пользователем.

Управление теперь готово. Чтобы протестировать его, создайте новый проект приложения Windows Phone Silverlight в контексте того же решения. Добавьте ссылку на ControlKit . В MainPage.xaml добавьте ссылку на сборку ControlKit, добавив дополнительное пространство имен:

xmlns:ckit="clr-namespace:ControlKit;assembly=ControlKit"

Теперь вы можете сделать что-то вроде этого:

<ckit:TapMenu>
    <ckit:TapMenuButton ImageSource="ApplicationIcon.png" Text="Test"></ckit:TapMenuButton>
    <ckit:TapMenuButton ImageSource="ApplicationIcon.png" Text="Test"></ckit:TapMenuButton>
    <ckit:TapMenuButton ImageSource="ApplicationIcon.png" Text="Test"></ckit:TapMenuButton>
    <ckit:TapMenuButton ImageSource="ApplicationIcon.png" Text="Test"></ckit:TapMenuButton>
    <ckit:TapMenuButton ImageSource="ApplicationIcon.png" Text="Test"></ckit:TapMenuButton>
    <ckit:TapMenuButton ImageSource="ApplicationIcon.png" Text="Test"></ckit:TapMenuButton>
    <ckit:TapMenuButton ImageSource="ApplicationIcon.png" Text="Test"></ckit:TapMenuButton>
    <ckit:TapMenuButton ImageSource="ApplicationIcon.png" Text="Test"></ckit:TapMenuButton>
    <ckit:TapMenuButton ImageSource="ApplicationIcon.png" Text="Test"></ckit:TapMenuButton>
</ckit:TapMenu>

Результат будет выглядеть примерно так:

Отлично, но Rowi показывает это меню всякий раз, когда пользователь нажимает на один из элементов в ListBox. Как вы можете видеть на изображении выше, я удобно реализовал список образцов с совершенно не относящимися к делу данными. Текущий ListBox XAML объявлен так:

<ListBox ItemsSource="{Binding Path=Instance.RandomTextItems,Source={StaticResource Binder}}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Grid Tap="Grid_Tap">
                <StackPanel Margin="0,0,0,30">
                    <TextBlock Style="{StaticResource PhoneTextGroupHeaderStyle}" Text="{Binding TextOne}"></TextBlock>
                    <TextBlock Style="{StaticResource PhoneTextSmallStyle}" Text="{Binding TextTwo}"></TextBlock>
                    <TextBlock Style="{StaticResource PhoneTextAccentStyle}" Text="{Binding TextThree}"></TextBlock>
                </StackPanel>
            </Grid>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Пример кода для TapMenu сверху можно легко перенести сюда в рабочей сетке DataTemplate. В отличие от StackPanel, элементы управления в Grid расположены друг над другом, если явно не указано иное. Это идеальный сценарий для того, что я собираюсь сделать:

<ListBox ItemsSource="{Binding Path=Instance.RandomTextItems,Source={StaticResource Binder}}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Grid Tap="Grid_Tap">
                <StackPanel Margin="0,0,0,30">
                    <TextBlock Style="{StaticResource PhoneTextGroupHeaderStyle}" Text="{Binding TextOne}"></TextBlock>
                    <TextBlock Style="{StaticResource PhoneTextSmallStyle}" Text="{Binding TextTwo}"></TextBlock>
                    <TextBlock Style="{StaticResource PhoneTextAccentStyle}" Text="{Binding TextThree}"></TextBlock>
                </StackPanel>

                <ckit:TapMenu x:Name="tapMenu" Visibility="Collapsed">
                    <ckit:TapMenuButton ImageSource="ApplicationIcon.png" Text="Test"></ckit:TapMenuButton>
                    <ckit:TapMenuButton ImageSource="ApplicationIcon.png" Text="Test"></ckit:TapMenuButton>
                    <ckit:TapMenuButton ImageSource="ApplicationIcon.png" Text="Test"></ckit:TapMenuButton>
                    <ckit:TapMenuButton ImageSource="ApplicationIcon.png" Text="Test"></ckit:TapMenuButton>
                    <ckit:TapMenuButton ImageSource="ApplicationIcon.png" Text="Test"></ckit:TapMenuButton>
                    <ckit:TapMenuButton ImageSource="ApplicationIcon.png" Text="Test"></ckit:TapMenuButton>
                    <ckit:TapMenuButton ImageSource="ApplicationIcon.png" Text="Test"></ckit:TapMenuButton>
                    <ckit:TapMenuButton ImageSource="ApplicationIcon.png" Text="Test"></ckit:TapMenuButton>
                    <ckit:TapMenuButton ImageSource="ApplicationIcon.png" Text="Test"></ckit:TapMenuButton>
                </ckit:TapMenu>
            </Grid>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

TapMenu теперь имеет имя и по умолчанию невидим. Сетка контейнера имеет обработчик событий, который обрабатывает событие TapGrid_Tap :

private void Grid_Tap(object sender, GestureEventArgs e)
{
    Grid mainGrid = sender as Grid;
    TapMenu menu = mainGrid.FindName("tapMenu") as TapMenu;
    if (menu.Visibility == System.Windows.Visibility.Collapsed)
        menu.Visibility = System.Windows.Visibility.Visible;
    else
        menu.Visibility = System.Windows.Visibility.Collapsed;
}

Если пользователь нажимает на сетку контейнера, а TapMenu не отображается, он отображается. В противном случае оно рухнет.

Вы можете добавить дополнительные обработчики событий для отдельных экземпляров TapMenuButton , например Click.

Визуальный вывод будет таким:

Вы можете скачать полный исходный код проекта на GitHub .