Monday, December 24, 2012

Notification Notch Control for Windows Phone

Many of you would have wondered why there is no Notification Notch in +Windows Phone, similar to the one seen first in Android and then later in iOS5 as well. However, there might be some restriction/limitation for them (WP team) to implement Notification Notch at this point of time but this shouldn't stop you from implementing it in your application. In today's article I am going to explain how to create a Notification Notch and how to use it in your application.

Well, our objective will be completed in 3 steps:

1. Create a custom behavior i.e. "Drag Notch Behavior" to mimic drag and expand/close nature of a notification notch.
2. Create a sample user control i.e. "Drag Notch Control" to use this behavior.
3. Create a sample project to use this control.

PREREQUISITE:  

1. Knowledge of Behavior in .Net

BUILDING HELPER CLASSES:

There are a few extension methods that are required to easily find and manipulate a FrameworkElemant from code. For doing so I've shamelessly pinched the code from "Joost van Schaik"  code article - "Simple Windows Phone 7 / Silverlight drag/flick behavior". These Libraries can be found under utilities in my sample code attached.








 










DRAG NOTCH BEHAVIOR:

Code for DragNotch Behavior is implemented by following class:
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Media;
using System.Windows.Media.Animation;
using nishantcop_Behaviors.Utilities;

namespace nishantcop_Behaviors.Behaviors
{
    public class DragNotchBehavior : Behavior
    {
        private FrameworkElement _elementToAnimate;
        public event NotchStateChangedHandler NotchStateChanged;

        public delegate void NotchStateChangedHandler(object sender, NotchStateChangedEventArgs e);

        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.Loaded += AssociatedObjectLoaded;
            AssociatedObject.ManipulationDelta += AssociatedObjectManipulationDelta;
            AssociatedObject.ManipulationCompleted += AssociatedObjectManipulationCompleted;

        }

        void AssociatedObjectLoaded(object sender, RoutedEventArgs e)
        {
            _elementToAnimate = AssociatedObject.GetElementToAnimate();
            if (!(_elementToAnimate.RenderTransform is CompositeTransform))
            {
                _elementToAnimate.RenderTransform = new CompositeTransform();
                _elementToAnimate.RenderTransformOrigin = new Point(0.5, 0.5);
            }
            StartPosition = AssociatedObject.GetTranslatePoint();
        }

        void AssociatedObjectManipulationDelta(object sender, ManipulationDeltaEventArgs e)
        {
            //var dx = e.DeltaManipulation.Translation.X;
            var dy = e.DeltaManipulation.Translation.Y;
            var currentPosition = _elementToAnimate.GetTranslatePoint();
            _elementToAnimate.SetTranslatePoint(currentPosition.X, currentPosition.Y + dy);
        }

        private void AssociatedObjectManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
        {
            // Create a storyboard that will emulate a 'flick'
            var currentPosition = _elementToAnimate.GetTranslatePoint();
            var velocity = e.FinalVelocities.LinearVelocity;
            if (velocity.Y > 0)
            {
                //var to = new Point(currentPosition.X, 360 + (velocity.Y / BrakeSpeed));
            }

            var storyboard = new Storyboard { FillBehavior = FillBehavior.HoldEnd };

            var to = new Point(currentPosition.X + (velocity.X / BrakeSpeed),
                currentPosition.Y + (velocity.Y / BrakeSpeed));
            storyboard.AddTranslationAnimation(_elementToAnimate, currentPosition, GetEndPoint(),
                new Duration(TimeSpan.FromMilliseconds(500)),
                new CubicEase { EasingMode = EasingMode.EaseOut });
            storyboard.Begin();
        }

        protected override void OnDetaching()
        {
            AssociatedObject.Loaded -= AssociatedObjectLoaded;
            AssociatedObject.ManipulationCompleted -= AssociatedObjectManipulationCompleted;
            AssociatedObject.ManipulationDelta -= AssociatedObjectManipulationDelta;
            base.OnDetaching();
        }

        private Point GetEndPoint()
        {
            double midPoint = Math.Abs(StartPosition.Y) - Math.Abs(_elementToAnimate.GetTranslatePoint().Y);
            if (midPoint < Math.Abs(StartPosition.Y) / 2)
            {
                
                NotchStateChanged(this, new NotchStateChangedEventArgs(false));
                return StartPosition;
            }
            else
            {
                IsNotchExpanded = true;
                NotchStateChanged(this, new NotchStateChangedEventArgs(true));
                return EndPosition;
            }
        }

        /// 
        /// This function tries to collapse a notch if it's expanded.
        /// 
        public void TryRollBackNotch()
        {
            var currentPosition = _elementToAnimate.GetTranslatePoint();
            if (currentPosition != StartPosition)
            {
                var storyboard = new Storyboard { FillBehavior = FillBehavior.HoldEnd };

                storyboard.AddTranslationAnimation(_elementToAnimate, currentPosition, StartPosition,
                    new Duration(TimeSpan.FromMilliseconds(500)),
                    new CubicEase { EasingMode = EasingMode.EaseOut });
                storyboard.Begin();
                storyboard.Completed += new EventHandler(storyboard_Completed);
            }
        }

        void storyboard_Completed(object sender, EventArgs e)
        {
            var storyboard = sender as Storyboard;
            storyboard.Completed -= storyboard_Completed;
            IsNotchExpanded = false;
            NotchStateChanged(this, new NotchStateChangedEventArgs(false));
        }

        #region IsNotchExpandedProperty
        public bool IsNotchExpanded
        {
            get { return (bool)GetValue(IsNotchExpandedProperty); }
            set { SetValue(IsNotchExpandedProperty, value); }
        }

        // Using a DependencyProperty as the backing store for IsNotchExpanded.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IsNotchExpandedProperty =
            DependencyProperty.Register("IsNotchExpanded", typeof(bool), typeof(DragNotchBehavior), new PropertyMetadata(false, OnIsNotchExpandedPropertyChanged));

        private static void OnIsNotchExpandedPropertyChanged(DependencyObject source,
        DependencyPropertyChangedEventArgs e)
        {
            DragNotchBehavior behavior = source as DragNotchBehavior;
            bool time = (bool)e.NewValue;
            // Put some update logic here...
        } 
        #endregion

        #region BrakeSpeed
        public const string BrakeSpeedPropertyName = "BrakeSpeed";

        /// 
        /// Describes how fast the element should brake, i.e. come to rest,
        /// after a flick. Higher = apply more brake ;-)
        /// 
        public int BrakeSpeed
        {
            get { return (int)GetValue(BrakeSpeedProperty); }
            set { SetValue(BrakeSpeedProperty, value); }
        }

        public static readonly DependencyProperty BrakeSpeedProperty = DependencyProperty.Register(
            BrakeSpeedPropertyName,
            typeof(int),
            typeof(DragNotchBehavior),
            new PropertyMetadata(10));

        #endregion

        #region StartPosition
        public Point StartPosition
        {
            get { return (Point)GetValue(StartPositionProperty); }
            set { SetValue(StartPositionProperty, value); }
        }

        // Using a DependencyProperty as the backing store for StartPosition.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty StartPositionProperty =
            DependencyProperty.Register("StartPosition", typeof(Point), typeof(DragNotchBehavior), new PropertyMetadata(new Point(0, 0)));
        #endregion

        #region End Position
        public Point EndPosition
        {
            get { return (Point)GetValue(EndPositionProperty); }
            set { SetValue(EndPositionProperty, value); }
        }

        // Using a DependencyProperty as the backing store for EndPosition.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty EndPositionProperty =
            DependencyProperty.Register("EndPosition", typeof(Point), typeof(DragNotchBehavior), new PropertyMetadata(new Point(0, 0))); 
        #endregion
        
    }
}

This behavior exposes following important properties:
  1. StartPosition: Element starts it's animation at this point. 
  2. EndPostion: Element ends it's animation at this point.
  3. BrakeSpeed: Speed by which animation should come to halt. 
  4. TryRollBackNotch:  A Function to force close/Rollback your Notification Notch control. 
  5. NotchStateChanged: An Event to notify the change in status of Notification Notch control i.e. closed or expanded.  
If you wish to fiddle around with it you can change the behavior to move horizontally only, contrasted to it's default vertical movement.

For adding a NotchStateChanged Event you will need to provide an customized EventArgs class. Code for which will look like this: 
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace nishantcop_Behaviors.Utilities
{
    public class NotchStateChangedEventArgs : EventArgs
    {
        private bool _IsNotchExpanded;

        public NotchStateChangedEventArgs(bool isNotchExpanded)
        {
            this._IsNotchExpanded = isNotchExpanded;
            //this.exceededPercentage = exceededPercentage;
            // not shown - validation for input
        }

        public bool IsNotchExpanded
        {
            get { return this._IsNotchExpanded; }
        }
    }
}

This class provides current state for your Notch Control. We handle this class in our Drag Notch Behavior whenever we are animating our control for expanding and closing it. 

So, that's about the Drag Notch Behavior now lets take a look into how exactly to use it in your project. 

DRAG NOTCH CONTROL

Now we need a DragNotch control that can be used across our application. A sample xaml for DragNotch control is: 


 
  
            
        
  
   
  
  
  
 
This control simply contains a Grid and our DragNotchBehavior is attached to it. For this custom behavior we need to set it's x:fieldModifier = "public" so that we can access it from code behind. Also we need to set the TranslateY property of CompositeTransform to a suitable position so that it won't be visible until it's being dragged. For this demo I've set this value to -700. However it can be easily tweaked as per your requirement.

Code behind for your DragNotch control:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace WPTestBehavior
{
 public partial class DragNotch : UserControl
 {
  public DragNotch()
  {
   // Required to initialize variables
   InitializeComponent();
  }

        private void Notch_NotchStateChanged(object sender, nishantcop_Behaviors.Utilities.NotchStateChangedEventArgs e)
        {
            bool b = e.IsNotchExpanded;
        }
        public void RollBackNotch()
        {
            Notch.TryRollBackNotch();
        }
 }
}

In code behind we are handling the NotchStateChanged event from our behavior and a public method (RollBackNotch) is exposed so that you can force your control to collapsed.

Now, your control is ready you can fill in whatever you want inside this grid, for your notification purpose or whatever you may want to call it :-)

USE DRAG NOTCH CONTROL

For using the DragNotch control in last step I've created this sample page:



    
    
        
            
            
        

        
        

        
        
         
         
         
        
    
This page contains ListBox filled with some sample data and our "TestDragNotch" control to overlay it when it's expanded to demonstrate the Notification Notch behavior.

VIDEO