A group blog from members of the VB team
In our last post, I explained how to implement a model-view-viewmodel pattern in a Windows Phone application. In this blog post, I want to share a sample that will help you to create a control tilt effect application for Windows Phone 7. This application will have a feature to add additional feedback for control interaction. It provides a “tilt”-like effect when a control is clicked or selected.
I will now demonstrate how easy it is to create a control tilt effect application for Windows Phone 7, using Visual Basic for Windows Phone Developer Tools. The control tilt effect application can be created in 4 simple steps as follows:
Prerequisites:
To create the control tilt effect application, let’s follow the 4 simple steps mentioned earlier:
<Button Width="186" Height="185" Content="Button" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="9,20,0,0" />
<Button Content="Button (Suppressed)" Height="150" HorizontalAlignment="Left" Margin="37,0,0,161" VerticalAlignment="Bottom" Width="380" local:TiltEffect.SuppressTilt="True"/>
<CheckBox Content="CheckBox" Height="72" HorizontalAlignment="Left" Margin="235,25,0,0" Name="checkBox1" VerticalAlignment="Top" />
<RadioButton Content="RadioButton" Height="72" HorizontalAlignment="Left" Margin="235,103,0,0" Name="radioButton1" VerticalAlignment="Top" />
<HyperlinkButton Content="HyperlinkButton" Height="30" HorizontalAlignment="Left" Margin="25,211,0,0" Name="hyperlinkButton1" VerticalAlignment="Top" Width="409" />
<ListBox Height="110" HorizontalAlignment="Left" Margin="6,472,0,0" Name="listBox1" VerticalAlignment="Top" Width="460" ItemsSource="{Binding}" >
<ListBoxItem Content="First ListBoxItem" ></ListBoxItem>
<ListBoxItem Content="Second ListBoxItem" ></ListBoxItem>
<ListBoxItem Content="Third ListBoxItem" ></ListBoxItem>
<ListBoxItem Content="Fourth ListBoxItem" ></ListBoxItem>
</ListBox>
xmlns:local="clr-namespace:ControlTiltEffect"
local:TiltEffect.IsTiltEnabled="True"
Adding event handlers is one of the important tasks. These event handlers are required to control the tilt effect in the container and the object.
Note: To add the event handlers, you can download the full source code of the application from here, and import the TiltEffect.vb file from the downloaded sample application. To import the file, do the following:
To create the TiltEffect.vb file and add the event handlers manually, do the following:
#If WINDOWS_PHONE Then
Imports Microsoft.Phone.Controls
#End If
''' <summary>
''' This code provides attached properties for adding a 'tilt' effect to all controls within a container.
''' </summary>
Public Class TiltEffect
Inherits DependencyObject
End Class
''' Couple of simple helpers for walking the visual tree
Friend Module TreeHelpers
''' Gets the ancestors of the element, up to the root
''' <param name="node">The element to start from</param>
''' <returns>An enumerator of the ancestors</returns>
<System.Runtime.CompilerServices.Extension()>
Public Function GetVisualAncestors(ByVal node As FrameworkElement) As IEnumerable(Of FrameworkElement)
Dim returnResult = New List(Of FrameworkElement)()
Dim parent = node.GetVisualParent()
Do While parent IsNot Nothing
returnResult.Add(parent)
parent = parent.GetVisualParent()
Loop
Return returnResult
End Function
''' Gets the visual parent of the element
''' <param name="node">The element to check</param>
''' <returns>The visual parent</returns>
Public Function GetVisualParent(ByVal node As FrameworkElement) As FrameworkElement
Return TryCast(VisualTreeHelper.GetParent(node), FrameworkElement)
End Module
#Region "Constructor and Static Constructor"
''' This is not a constructable class, but it cannot be static because it derives from DependencyObject.
Private Sub New()
End Sub
''' Initialize the static properties
Shared Sub New()
' The tiltable items list.
TiltableItems = New List(Of Type) From {GetType(ButtonBase), GetType(ListBoxItem)}
UseLogarithmicEase = False
#End Region
#Region "Fields and simple properties"
' These constants are the same as the built-in effects
''' Maximum amount of tilt, in radians
Private Const MaxAngle = 0.3
''' Maximum amount of depression, in pixels
Private Const MaxDepression = 25.0
''' Delay between releasing an element and the tilt release animation playing
Private Shared ReadOnly TiltReturnAnimationDelay As TimeSpan =
TimeSpan.FromMilliseconds(200)
''' Duration of tilt release animation
Private Shared ReadOnly TiltReturnAnimationDuration As TimeSpan =
TimeSpan.FromMilliseconds(100)
''' The control that is currently being tilted
Private Shared currentTiltElement As FrameworkElement
''' The single instance of a storyboard used for all tilts
Private Shared tiltReturnStoryboard As Storyboard
''' The single instance of an X rotation used for all tilts
Private Shared tiltReturnXAnimation As DoubleAnimation
''' The single instance of a Y rotation used for all tilts
Private Shared tiltReturnYAnimation As DoubleAnimation
''' The single instance of a Z depression used for all tilts
Private Shared tiltReturnZAnimation As DoubleAnimation
''' The center of the tilt element
Private Shared currentTiltElementCenter As Point
''' Whether the animation just completed was for a 'pause' or not
Private Shared wasPauseAnimation As Boolean = False
''' Whether to use a slightly more accurate (but slightly slower) tilt animation easing function
Public Shared Property UseLogarithmicEase() As Boolean
''' Default list of items that are tiltable
Private Shared _tiltableItems As List(Of Type)
Public Shared Property TiltableItems() As List(Of Type)
Get
Return _tiltableItems
End Get
Private Set(ByVal value As List(Of Type))
_tiltableItems = value
End Set
End Property
#Region "Dependency properties"
''' Whether the tilt effect is enabled on a container (and all its children)
Public Shared ReadOnly IsTiltEnabledProperty As DependencyProperty =
DependencyProperty.RegisterAttached("IsTiltEnabled",
GetType(Boolean),
GetType(TiltEffect),
New PropertyMetadata(AddressOf OnIsTiltEnabledChanged))
''' Gets the IsTiltEnabled dependency property from an object
''' <param name="source">The object to get the property from</param>
''' <returns>The property's value</returns>
Public Shared Function GetIsTiltEnabled(ByVal source As DependencyObject) As Boolean
Return CBool(source.GetValue(IsTiltEnabledProperty))
''' Sets the IsTiltEnabled dependency property on an object
''' <param name="source">The object to set the property on</param>
''' <param name="value">The value to set</param>
Public Shared Sub SetIsTiltEnabled(ByVal source As DependencyObject, ByVal value As Boolean)
source.SetValue(IsTiltEnabledProperty, value)
''' Suppresses the tilt effect on a single control that would otherwise be tilted
Public Shared ReadOnly SuppressTiltProperty As DependencyProperty =
DependencyProperty.RegisterAttached("SuppressTilt",
Nothing)
''' Gets the SuppressTilt dependency property from an object
Public Shared Function GetSuppressTilt(ByVal source As DependencyObject) As Boolean
Return CBool(source.GetValue(SuppressTiltProperty))
''' Sets the SuppressTilt dependency property from an object
Public Shared Sub SetSuppressTilt(ByVal source As DependencyObject,
ByVal value As Boolean)
source.SetValue(SuppressTiltProperty, value)
''' Property change handler for the IsTiltEnabled dependency property
''' <param name="target">The element that the property is atteched to</param>
''' <param name="args">Event args</param>
''' <remarks>
''' Adds or removes event handlers from the element that has been (un)registered for tilting
''' </remarks>
Private Shared Sub OnIsTiltEnabledChanged(ByVal target As DependencyObject,
ByVal args As DependencyPropertyChangedEventArgs)
If TypeOf target Is FrameworkElement Then
' Add / remove the event handler if necessary
If CBool(args.NewValue) = True Then
AddHandler TryCast(target, FrameworkElement).ManipulationStarted,
AddressOf TiltEffect_ManipulationStarted
Else
RemoveHandler TryCast(target, FrameworkElement).ManipulationStarted,
End If
#Region "Top-level manipulation event handlers"
''' Event handler for ManipulationStarted
''' <param name="sender">sender of the event - this will be the tilt container (eg, entire page)</param>
''' <param name="e">event args</param>
Private Shared Sub TiltEffect_ManipulationStarted(ByVal sender As Object,
ByVal e As ManipulationStartedEventArgs)
TryStartTiltEffect(TryCast(sender, FrameworkElement), e)
''' Event handler for ManipulationDelta
''' <param name="sender">sender of the event - this will be the tilting object (eg a button)</param>
Private Shared Sub TiltEffect_ManipulationDelta(ByVal sender As Object,
ByVal e As ManipulationDeltaEventArgs)
ContinueTiltEffect(TryCast(sender, FrameworkElement), e)
''' Event handler for ManipulationCompleted
Private Shared Sub TiltEffect_ManipulationCompleted(ByVal sender As Object,
ByVal e As ManipulationCompletedEventArgs)
EndTiltEffect(currentTiltElement)
#Region "Core tilt logic"
#Region "Custom easing function"
''' Provides an easing function for the tilt return
Private Class LogarithmicEase
Inherits EasingFunctionBase
''' Computes the easing function
''' <param name="normalizedTime">The time</param>
''' <returns>The eased value</returns>
Protected Overrides Function EaseInCore(ByVal normalizedTime As Double) As Double
Return Math.Log(normalizedTime + 1) / 0.693147181 ' ln(t + 1) / ln(2)
''' Checks if the manipulation should cause a tilt, and if so starts the tilt effect
''' <param name="source">The source of the manipulation (the tilt container, eg entire page)</param>
''' <param name="e">The args from the ManipulationStarted event</param>
Private Shared Sub TryStartTiltEffect(ByVal source As FrameworkElement, ByVal e As ManipulationStartedEventArgs)
For Each ancestor In (TryCast(e.OriginalSource, FrameworkElement)).GetVisualAncestors()
For Each t As Type In TiltableItems
If t.IsAssignableFrom(ancestor.GetType()) Then
If CBool(ancestor.GetValue(SuppressTiltProperty)) <> True Then
' Use first child of the control, so that you can add transforms and not
' impact any transforms on the control itself
Dim element = TryCast(VisualTreeHelper.GetChild(ancestor, 0), FrameworkElement)
Dim container = TryCast(e.ManipulationContainer, FrameworkElement)
If element Is Nothing OrElse container Is Nothing Then
Return
' Touch point relative to the element being tilted
Dim tiltTouchPoint = container.TransformToVisual(element).Transform(e.ManipulationOrigin)
' Center of the element being tilted
Dim elementCenter As New Point(element.ActualWidth / 2,
element.ActualHeight / 2)
' Camera adjustment
Dim centerToCenterDelta = GetCenterToCenterDelta(element, source)
BeginTiltEffect(element, tiltTouchPoint, elementCenter, centerToCenterDelta)
Next t
Next ancestor
''' Computes the delta between the centre of an element and its container
''' <param name="element">The element to compare</param>
''' <param name="container">The element to compare against</param>
''' <returns>A point that represents the delta between the two centers</returns>
Private Shared Function GetCenterToCenterDelta(ByVal element As FrameworkElement,
ByVal container As FrameworkElement) As Point
Dim elementCenter As New Point(element.ActualWidth / 2, element.ActualHeight / 2)
Dim containerCenter As Point
' Need to special-case the frame to handle different orientations
If TypeOf container Is PhoneApplicationFrame Then
Dim frame = TryCast(container, PhoneApplicationFrame)
' Switch width and height in landscape mode
If (frame.Orientation And PageOrientation.Landscape) = PageOrientation.Landscape Then
containerCenter = New Point(container.ActualHeight / 2, container.ActualWidth / 2)
containerCenter = New Point(container.ActualWidth / 2, container.ActualHeight / 2)
#Else
Dim transformedElementCenter = element.TransformToVisual(container).Transform(elementCenter)
Dim result As New Point(containerCenter.X - transformedElementCenter.X,
containerCenter.Y - transformedElementCenter.Y)
Return result
''' Begins the tilt effect by preparing the control and doing the initial animation
''' <param name="element">The element to tilt </param>
''' <param name="touchPoint">The touch point, in element coordinates</param>
''' <param name="centerPoint">The center point of the element in element coordinates</param>
''' <param name="centerDelta">The delta between the <paramref name="element"/>'s center and
''' the container's center</param>
Private Shared Sub BeginTiltEffect(ByVal element As FrameworkElement,
ByVal touchPoint As Point,
ByVal centerPoint As Point,
ByVal centerDelta As Point)
If tiltReturnStoryboard IsNot Nothing Then
StopTiltReturnStoryboardAndCleanup()
If PrepareControlForTilt(element, centerDelta) = False Then
currentTiltElement = element
currentTiltElementCenter = centerPoint
PrepareTiltReturnStoryboard(element)
ApplyTiltEffect(currentTiltElement, touchPoint, currentTiltElementCenter)
''' Prepares a control to be tilted by setting up a plane projection and some event handlers
''' <param name="element">The control that is to be tilted</param>
''' <param name="centerDelta">Delta between the element's center and the tilt container's</param>
''' <returns>true if successful; false otherwise</returns>
''' This method is conservative; it will fail any attempt to tilt a control that already
''' has a projection on it
Private Shared Function PrepareControlForTilt(ByVal element As FrameworkElement,
ByVal centerDelta As Point) As Boolean
' Prevents interference with any existing transforms
If element.Projection IsNot Nothing OrElse
(element.RenderTransform IsNot Nothing AndAlso
element.RenderTransform.GetType() IsNot GetType(MatrixTransform)) Then
Return False
Dim transform As New TranslateTransform()
transform.X = centerDelta.X
transform.Y = centerDelta.Y
element.RenderTransform = transform
Dim projection As New PlaneProjection()
projection.GlobalOffsetX = -1 * centerDelta.X
projection.GlobalOffsetY = -1 * centerDelta.Y
element.Projection = projection
AddHandler element.ManipulationDelta, AddressOf TiltEffect_ManipulationDelta
AddHandler element.ManipulationCompleted, AddressOf TiltEffect_ManipulationCompleted
Return True
''' Removes modifications made by PrepareControlForTilt
''' <param name="element">THe control to be un-prepared</param>
''' This method is basic; it does not do anything to detect if the control being un-prepared
''' was previously prepared
Private Shared Sub RevertPrepareControlForTilt(ByVal element As FrameworkElement)
RemoveHandler element.ManipulationDelta, AddressOf TiltEffect_ManipulationDelta
RemoveHandler element.ManipulationCompleted, AddressOf TiltEffect_ManipulationCompleted
element.Projection = Nothing
element.RenderTransform = Nothing
''' Creates the tilt return storyboard (if not already created) and targets it to the projection
Private Shared Sub PrepareTiltReturnStoryboard(ByVal element As FrameworkElement)
If tiltReturnStoryboard Is Nothing Then
tiltReturnStoryboard = New Storyboard()
AddHandler tiltReturnStoryboard.Completed, AddressOf TiltReturnStoryboard_Completed
tiltReturnXAnimation = New DoubleAnimation()
Storyboard.SetTargetProperty(tiltReturnXAnimation,
New PropertyPath(PlaneProjection.RotationXProperty))
tiltReturnXAnimation.BeginTime = TiltReturnAnimationDelay
tiltReturnXAnimation.To = 0
tiltReturnXAnimation.Duration = TiltReturnAnimationDuration
tiltReturnYAnimation = New DoubleAnimation()
Storyboard.SetTargetProperty(tiltReturnYAnimation,
New PropertyPath(PlaneProjection.RotationYProperty))
tiltReturnYAnimation.BeginTime = TiltReturnAnimationDelay
tiltReturnYAnimation.To = 0
tiltReturnYAnimation.Duration = TiltReturnAnimationDuration
tiltReturnZAnimation = New DoubleAnimation()
Storyboard.SetTargetProperty(tiltReturnZAnimation,
New PropertyPath(PlaneProjection.GlobalOffsetZProperty))
tiltReturnZAnimation.BeginTime = TiltReturnAnimationDelay
tiltReturnZAnimation.To = 0
tiltReturnZAnimation.Duration = TiltReturnAnimationDuration
If UseLogarithmicEase Then
tiltReturnXAnimation.EasingFunction = New LogarithmicEase()
tiltReturnYAnimation.EasingFunction = New LogarithmicEase()
tiltReturnZAnimation.EasingFunction = New LogarithmicEase()
tiltReturnStoryboard.Children.Add(tiltReturnXAnimation)
tiltReturnStoryboard.Children.Add(tiltReturnYAnimation)
tiltReturnStoryboard.Children.Add(tiltReturnZAnimation)
Storyboard.SetTarget(tiltReturnXAnimation, element.Projection)
Storyboard.SetTarget(tiltReturnYAnimation, element.Projection)
Storyboard.SetTarget(tiltReturnZAnimation, element.Projection)
''' Continues a tilt effect that is currently applied to an element, presumably because
''' the user moved their finger
''' <param name="element">The element being tilted</param>
''' <param name="e">The manipulation event args</param>
Private Shared Sub ContinueTiltEffect(ByVal element As FrameworkElement,
Dim container As FrameworkElement = TryCast(e.ManipulationContainer, FrameworkElement)
If container Is Nothing OrElse element Is Nothing Then
Dim tiltTouchPoint As Point = container.TransformToVisual(element).Transform(e.ManipulationOrigin)
' If touch moved outside bounds of element, then pause the tilt (but don't cancel it)
If New Rect(0,
0,
currentTiltElement.ActualWidth,
currentTiltElement.ActualHeight).Contains(tiltTouchPoint) <> True Then
PauseTiltEffect()
' Apply the updated tilt effect
ApplyTiltEffect(currentTiltElement, e.ManipulationOrigin, currentTiltElementCenter)
''' Ends the tilt effect by playing the animation
Private Shared Sub EndTiltEffect(ByVal element As FrameworkElement)
If element IsNot Nothing Then
wasPauseAnimation = False
If tiltReturnStoryboard.GetCurrentState() <> ClockState.Active Then
tiltReturnStoryboard.Begin()
''' Handler for the storyboard complete event
''' <param name="sender">sender of the event</param>
Private Shared Sub TiltReturnStoryboard_Completed(ByVal sender As Object, ByVal e As EventArgs)
If wasPauseAnimation Then
ResetTiltEffect(currentTiltElement)
''' Resets the tilt effect on the control, making it appear 'normal' again
''' <param name="element">The element to reset the tilt on</param>
''' This method doesn't turn off the tilt effect or cancel any current
''' manipulation; it just temporarily cancels the effect
Private Shared Sub ResetTiltEffect(ByVal element As FrameworkElement)
Dim projection As PlaneProjection = TryCast(element.Projection, PlaneProjection)
projection.RotationY = 0
projection.RotationX = 0
projection.GlobalOffsetZ = 0
''' Stops the tilt effect and release resources applied to the currently-tilted control
Private Shared Sub StopTiltReturnStoryboardAndCleanup()
tiltReturnStoryboard.Stop()
RevertPrepareControlForTilt(currentTiltElement)
''' Pauses the tilt effect so that the control returns to the 'at rest' position, but doesn't
''' stop the tilt effect (handlers are still attached, etc.)
Private Shared Sub PauseTiltEffect()
If (tiltReturnStoryboard IsNot Nothing) AndAlso (Not wasPauseAnimation) Then
wasPauseAnimation = True
''' Resets the storyboard to not running
Private Shared Sub ResetTiltReturnStoryboard()
''' Applies the tilt effect to the control
''' <param name="element">the control to tilt</param>
''' <param name="touchPoint">The touch point, in the container's coordinates</param>
''' <param name="centerPoint">The center point of the container</param>
Private Shared Sub ApplyTiltEffect(ByVal element As FrameworkElement,
ByVal centerPoint As Point)
' Stop any active animation
ResetTiltReturnStoryboard()
' Get relative point of the touch in percentage of container size
Dim normalizedPoint As New Point(Math.Min(Math.Max(touchPoint.X / (centerPoint.X * 2), 0), 1),
Math.Min(Math.Max(touchPoint.Y / (centerPoint.Y * 2), 0), 1))
' Shell values
Dim xMagnitude = Math.Abs(normalizedPoint.X - 0.5)
Dim yMagnitude = Math.Abs(normalizedPoint.Y - 0.5)
Dim xDirection = -Math.Sign(normalizedPoint.X - 0.5)
Dim yDirection = Math.Sign(normalizedPoint.Y - 0.5)
Dim angleMagnitude = xMagnitude + yMagnitude
Dim xAngleContribution = If(xMagnitude + yMagnitude > 0,
xMagnitude / (xMagnitude + yMagnitude),
0)
Dim angle = angleMagnitude * MaxAngle * 180 / Math.PI
Dim depression = (1 - angleMagnitude) * MaxDepression
' RotationX and RotationY are the angles of rotations about the x- or y-*axis*;
' to achieve a rotation in the x- or y-*direction*, we need to swap the two.
' That is, a rotation to the left about the y-axis is a rotation to the left in the x-direction,
' and a rotation up about the x-axis is a rotation up in the y-direction.
projection.RotationY = angle * xAngleContribution * xDirection
projection.RotationX = angle * (1 - xAngleContribution) * yDirection
projection.GlobalOffsetZ = -depression
Voila! Now your control tilt effect application for Windows Phone 7 is ready! You just need to build and debug the application.
Note: To stop debugging the application, select Debug > Stop Debugging.
Finally, to submit your application to the market place, you can refer to upload your application walkthrough.
That’s it! You have now successfully created the control tilt effect application for Windows Phone 7, that too in just 4 simple steps!
You can find the full source code for the Visual Basic Control Tilt Effect application here.