The holidays are long gone and now it’s time for me to end this blog series. In the previous post was the last post about actually implementing the ISurfaceScrollInfo interface, but I wanted to end this with talking about the solution for SurfacePagePanel.
The behavior of the SurfacePagePanel is to only show one page (list item) at a time, or as far as it can display only one page. To do that I need the SurfacePagePanel to take control of the panning between pages. If you remember from my last post, I mentioned that I implemented a “peak” functionality. Peaking allows the user to look at the adjacent pages but more with a rubber band kind of feeling. I think you need the rubber band feeling on a Microsoft Surface because:
- The area of use is larger.
- The panel is probably not constraint by a physical border, like the edge of a mobile device.
How is the peaking functionality implemented in SurfacePagePanel? Although I mentioned the solution in the Part Three, I had to rewrite the code.Why? Because I didn’t understand it! ;). Nothing made sense to me when I read the code so I ended up rewriting it. However, the idea is the same as before, to keep the x-value of the output vector within a certain range. In my code I use a logarithmic function to cap x-values. But that is not all. To make the explanation easier I start with showing of a graph of two curves:
figure 1: logarithmic and linear curve
Well, the curves represents how the corresponding mathematical function maps the input value to an output value, in our case mapping the x-value from input to the output vector. If I only were to use the logarithmic function the result would be that the panning would go faster than the contact movement at the beginning of the panning, because of inclination of the curve. Therefore I mixed in a linear curve. The idea is to let the linear curve control the mapping of the x-value until a crossing point (where the two curves intersect). After that I use the logarithmic function. To control the crossing point, or the intersection, I alter the altitude of the logarithmic curve by multiplying the function with a specified factor. In the graph above I’ve used a factor of 30. This means that when the x-value reaches 60, the the logarithmic function seize control of the mapping. This is how it looks In code:
236 public Vector ConvertToViewportUnits(Point origin, Vector offset)
237 {
238 if (_isMoving || !_panningOrigin.HasValue)
239 {
240 return new Vector(0.0, 0.0);
241 }
242
243 const int logBase = 2;
244 const double scaleFactor = 0.2;
245
246 var elasticityLength = GetScrollOwnerElasticityLength() * scaleFactor;
247 var absHorizontalOffset = Math.Abs(offset.X);
248 var direction = offset.X / absHorizontalOffset;
249 absHorizontalOffset *= scaleFactor;
250 var thresholdFactor = elasticityLength/Math.Log(elasticityLength, logBase);
251 var cappedOffset = Math.Min(absHorizontalOffset, Math.Log(absHorizontalOffset, logBase) * thresholdFactor);
252
253 return new Vector(cappedOffset * direction, offset.Y);
254 }
At line 250 I determine the crossing point factor of the logarithmic function using the Elasticity property of the ScrollOwner. That’s how the peak function is implemented.
To change page the user can either peak far enough or use a flick gesture. Doing that I listen to the ContactUp event in the SurfacePagePanel. Look at the code executed on the event:
610 private void OnScrollOwnerContactUp(object sender, ContactEventArgs e)
611 {
612 //The first contact has been captured.
613 if (_isMoving || !e.Contact.IsFingerRecognized ||
614 e.Contact.IsTagRecognized || _scrollOwner.ContactsCaptured.Count > 1 || !_panningOrigin.HasValue)
615 {
616 return;
617 }
618
619 var point = e.GetPosition(ScrollOwner);
620 var destinationIndex = DetermineNextFocusedChildIndex(point);
621 _panningOrigin = null;
622 e.Handled = true;
623 MoveViewportToChild(destinationIndex);
624 }
Essentially; first I get the the page that I will move to, which can either be the next, previous or the current one. Second I programmatically pan to that page. That is done using a KeyFrame animation. For the moment I inserted a “bounce” effect just like on the iPhone and the Android and the code for doing all this looks like this:
737 private AnimationTimeline BuildMovementAnimation(double offset, double direction, Duration animationDuration)
738 {
739 var turningPointTime = TimeSpan.FromMilliseconds(animationDuration.TimeSpan.TotalMilliseconds * 0.7);
740 var turningPointOffset = offset + (direction * GetBounceElasticityLength());
741 var destinationOffset = offset;
742
743 var animation = new DoubleAnimationUsingKeyFrames { Duration = animationDuration };
744 var startFrame = new SplineDoubleKeyFrame(HorizontalOffset, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.0)));
745 var turningPointFrame = new SplineDoubleKeyFrame(turningPointOffset, KeyTime.FromTimeSpan(turningPointTime), new KeySpline(0.8, 0.8, 0.0, 1.0));
746 var endFrame = new SplineDoubleKeyFrame(destinationOffset, KeyTime.FromTimeSpan(animationDuration.TimeSpan), new KeySpline(0.5, 1.0, 0.5, 1.0));
747
748 animation.KeyFrames.Add(startFrame);
749 animation.KeyFrames.Add(turningPointFrame);
750 animation.KeyFrames.Add(endFrame);
751
752 return animation;
753 }
I’m sorry for the code formatting, but once again I blame the blog theme ;). As you see, the bounce always occur after 70% of the animation duration.
Well, that concludes this blog series about how I implemented ISurfaceScrollInfo for the SurfacePagePanel. I hoped you liked it and happy “surfacing”.