Smooth Scrolling HorizontalList

This example is based on Alex Harui’s Smooth Scrolling List. It just changes the code to work with the HorizontalList control rather than the List control. The SmoothHorizontalScrollingList class extends the HorizontalList control adding the needed functionality to allow smooth scrolling:

Example

Source file: SmoothScrollingHorizontalList.zip

Below is the full code for the example.

Code

 package com.custom.controls
 {
 import flash.display.DisplayObject;
 import flash.events.Event;
 import flash.events.MouseEvent;

import mx.controls.HorizontalList;
 import mx.core.ScrollPolicy;
 import mx.effects.easing.*;
 import mx.events.ScrollEvent;
 import mx.events.ScrollEventDetail;

/**
 * List that uses smooth scrolling
 */
 public class SmoothHorizontalScrollingList extends HorizontalList
 {

private var fudge:Number;

public function SmoothHorizontalScrollingList()
 {
 super();
 offscreenExtraRowsOrColumns = 2;
 }

override protected function configureScrollBars():void
 {
 super.configureScrollBars();
 if (horizontalScrollBar)
 horizontalScrollBar.lineScrollSize = .125; // should be inverse power of 2
 }

override public function get horizontalScrollPosition():Number
 {
 if (!isNaN(fudge))
 {
 var vsp:Number = super.horizontalScrollPosition + fudge;
 fudge = NaN;
 return vsp;
 }
 return Math.floor(super.horizontalScrollPosition);
 }

override protected function scrollHandler(event:Event):void
 {
 // going backward is trickier. When you cross from, for instance 2.1 to 1.9, you need to convince
 // the superclass that it is going from 2 to 1 so the delta is -1 and not -.2.
 // we do this by adding a fudge factor to the first return from horizontalScrollPosition
 // which is used by the superclass logic.
 var last:Number = super.horizontalScrollPosition;
 var vsp:Number = this.horizontalScrollBar.scrollPosition;
 if (vsp < last)
 {
 if (last != Math.floor(last) || vsp != Math.floor(vsp))
 {
 if (Math.floor(vsp) < Math.floor(last))
 {
 fudge = Math.floor(last) - Math.floor(horizontalScrollBar.scrollPosition);
 trace(last.toFixed(2), vsp.toFixed(2), fudge);
 }
 }
 }

super.scrollHandler(event);
 var pos:Number = super.horizontalScrollPosition;
 // if we get a THUMB_TRACK, then we need to calculate the position
 // because it gets rounded to an int by the ScrollThumb code, and
 // we want fractional values.
 if (event is ScrollEvent)
 {
 var se:ScrollEvent = ScrollEvent(event);
 if (se.detail == ScrollEventDetail.THUMB_TRACK)
 {
 if (horizontalScrollBar.numChildren == 4)
 {
 var downArrow:DisplayObject = horizontalScrollBar.getChildAt(3);
 var thumb:DisplayObject = horizontalScrollBar.getChildAt(2);
 pos = (thumb.y - downArrow.height) / (downArrow.y - thumb.height - downArrow.height) * this.maxHorizontalScrollPosition;
 // round to nearest lineScrollSize;
 pos /= horizontalScrollBar.lineScrollSize;
 pos = Math.round(pos);
 pos *= horizontalScrollBar.lineScrollSize;
 //trace("faked", pos);
 }
 }
 }
 var fraction:Number = pos - horizontalScrollPosition;
 fraction *= this.columnWidth;
 //trace("was", listContent.y.toFixed(2));
 listContent.move(viewMetrics.left + listContent.leftOffset - fraction, listContent.y);
 //trace("now", listContent.y.toFixed(2), fraction.toFixed(2), listItems[0][0].data.lastName);
 }
 }
 }
 

The following code is an example of how to use the custom control in a Flex application:

<?xml version="1.0" encoding="utf-8"?>
 <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:controls="com.custom.controls.*" creationComplete="init()" height="383" width="388">
     <mx:Script>
         <![CDATA[
             import mx.collections.ArrayCollection;

             [Bindable]private var _dataProvider:ArrayCollection;

             private function init():void
             {
                 _dataProvider = new ArrayCollection();
                 _dataProvider.addItem({title:"Louis Leakey"});
                 _dataProvider.addItem({title:"Mary Leakey"});
                 _dataProvider.addItem({title:"Richard Leakey"});
                 _dataProvider.addItem({title:"Margaret Mead"});
                 _dataProvider.addItem({title:"Jane Goodall"});
                 _dataProvider.addItem({title:"Ruth Benedict"});
                 _dataProvider.addItem({title:"Franz Boas"});
                 _dataProvider.addItem({title:"Claude Levi Strauss"});
                 _dataProvider.addItem({title:"Noam Chomsky"});
                 _dataProvider.addItem({title:"James Deetz "});
                 _dataProvider.addItem({title:"Sir Arthur Evans"});
                 _dataProvider.addItem({title:"Pere Bosch-Gimpera"});
                 _dataProvider.addItem({title:"Kathleen Kenyon"});
                 _dataProvider.addItem({title:"Colin Renfrew"});
                 _dataProvider.addItem({title:"Mortimer Wheeler"});
                 _dataProvider.addItem({title:"Leonard Woolley"});
                 _dataProvider.addItem({title:"Lewis Binford"});
                 _dataProvider.addItem({title:"Howard Carte"});
                 _dataProvider.addItem({title:"Donald Johansen"});
                 _dataProvider.addItem({title:"Phil Harding"});
                 _dataProvider.addItem({title:"Alfred Foucher"});
                 _dataProvider.addItem({title:"Heinrich Schliemann"});
                 _dataProvider.addItem({title:"Mick Aston"});
                 _dataProvider.addItem({title:"Barry Cunliffe"});
                 _dataProvider.addItem({title:"Churchill Babington"});
                 _dataProvider.addItem({title:"Vere Gordon Childe"});
                 _dataProvider.addItem({title:"Gustaf VI Adolf"});
             }

             public function getTitle(data:Object):String
             {
                 return data.title;
             }
         ]]>
     </mx:Script>

     <mx:HBox width="100%" height="200">
         <controls:SmoothHorizontalScrollingList
             dataProvider="{_dataProvider}"
             width="100%" height="100%">
             <controls:itemRenderer>
                 <mx:Component>
                     <mx:Text text="{outerDocument.getTitle(data)}"/>
                 </mx:Component>
             </controls:itemRenderer>
         </controls:SmoothHorizontalScrollingList>
     </mx:HBox>
 </mx:Application>

Good, but…

What I would like to do is make it so the list scrolls smoothtly without the the horizontal scroll bars visible. However, I can’t yet figure out how to do this by changing just the horizontalScrollPosition, it seems to revert back to the default behavior for scrolling or blows up when you scroll from the right back to the left. I quickly began to see that to get a smooth scrolling list within Flex you would have to use an VBox or HBox container with a repeater. This method is outlined by Peter Ent “itemRenderers: Part 4: States and Transitions”.

Update

Found an interesting post for overriding the scrollToIndex method of the HorizontalList control to allow for animated scrolling using external buttons, the only issue is that I couldn’t scroll from the end of the list back to the beginning. I also found a blog post about built in animation effects for Flex 3 List controls using mx.effects.DefaultTileListEffect. However, it seems you need to use this only for a List or TileList component rather than a HorizontalList: itemsChangeEffect=”{mx.effects.DefaultListEffect}” . I have not tried this method yet to animate a list, but it may prove promising if its actually what it claims to be.

Another Solution

Ben Clinkinbeard of “returned undefined;” blog and UM fame posted a great little example of a Smooth Scrolling List that takes a List control and wraps it in a Canvas container to achieve the smooth scrolling effect. This technique has the benefit of keeping the functionality of the list but also allowing you to smoothly scroll the list by using and external tween. It should be easy to convert this to a horizontal list and retain the smooth scrolling effect. Of course, the downside to this method is that you have to render the entire list, which wouldn’t be very pleasant for large data sets, but for my needs this works great since my data set is very small. I have also experienced some resize issues when deleting items from the list. Here is an example of a Smooth Scrolling HorizontalList using a modified version of Ben’s technique:

The code for the SmoothScrollingHorizontalList class:

 /**
 * Original code from Ben Clinkinbeard (http://www.returnundefined.com/2009/03/smooth-scrolling-flex-list);
 * */
 package com.benclinkinbeard.controls
 {
 import mx.controls.HorizontalList;
 import mx.core.Container;
 import mx.core.ScrollPolicy;
 import mx.core.mx_internal;
 import mx.events.FlexEvent;

public class SmoothScrollingHorizontalList extends HorizontalList
 {
 public function SmoothScrollingHorizontalList()
 {
 super();

// required to ensure all renderers get created
 setStyle( "paddingRight", -1 );

// parent container will handle scrolling
 horizontalScrollPolicy = ScrollPolicy.OFF;

addEventListener( FlexEvent.UPDATE_COMPLETE, handleUpdateComplete );
 }

private function handleUpdateComplete( event:FlexEvent ):void
 {
 var combinedRendererWidth:Number = 0;

// iterate over list of renderers provided by our List subclass
 // but since HorozintalList does not have variable column widths, we use columnWidth
 for each( var renderer:Object in renderers )
 {
 combinedRendererWidth += columnWidth;
 }

// list needs to be at least 10 pixels wide to show results
 // and always needs to be 10 pixels wider than the combined width of the renderers
 width = combinedRendererWidth + 10;

// need to shrink list height when canvas has a scrollbar so the scrollbar doesn't overlap the list
 height = ( Container( parent ).maxHorizontalScrollPosition > 0 ) ? parent.height - 16 : parent.height;

// set the row height to height of list
 rowHeight = height;
 }

// array of renderers being used in this list
 public function get renderers():Array
 {
 // prefix the internal property name with its namespace
 var rawArray:Array = mx_internal::rendererArray;
 var arr:Array = new Array();

// the rendererArray is a bit messy
 // its an Array of Arrays, except sometimes the sub arrays are empty
 // and sometimes it contains entries that aren't Arrays at all
 for each( var obj:Object in rawArray )
 {
 var rendererArray:Array = obj as Array;

// make sure we have an Array and there is something in it
 if( rendererArray && rendererArray.length > 0 )
 {
 // if there is something in it, the first item is our renderer
 // but this doesn't seem to be the case for HorizontalList because obj[0] is always empty?
 //arr.push( obj[ 0 ] );
 arr = rendererArray;
 }
 }

return arr;
 }
 }
 }
 

The source code for the example: SmoothScrollingHorizontalList2.zip

-Mister

16 Comments

  1. great work! i’m going to use it in my application.
    btw there is mistake in link to second zip 😉

  2. Great Post… What about when the collection changes (ie it’s filer or items are removed). The scroll doesn’t update. Any idea on how to adjust the scrollbar when the collection changes – specifically when items are removed?

  3. This is great but it didn’t seem to work for me. The container would cutoff some of my entries. Anyways, the concept is great and I was able to apply the same exact technique.

    Thanks!

  4. Great insight. I tried very hard to get it to work, but kept running into problems w/ the canvas not adding the hlist, so I tried a calllater() to eventually add it, but then thought, instead of scrolling the canvas, why not just move my hlist’s x and tween the movement…made mention of it here. Thanks for the hints! It got me rolling!

  5. Thanks for this solution. I am trying to make use of it in a current project. However, it seems that flex isn’t finding the HorizontalList class. I added the src folder containing the mx libraries, to the source path, thinking this would solve the problem, but instead it added new errors, i.e. “Unable to resolve resource bundle “collections” for locale “en_US”.

    This is really frustrating as I assumed the HorizontalList would naturally be included in the sdk (actually it is. the compiler just isn’t seeing it) and I’m not seeing any solutions to this problem after hours of googling and testing. Any ideas?

    1. Sounds like the project is hosed. Try creating a new Flex project and recreating the example with your own MXML and class files. You can just view source of the example to see the code.

  6. Thanks for this solution. I am trying to make use of it in a current project. However, it seems that flex isn’t finding the HorizontalList class. I added the src folder containing the mx libraries, to the source path, thinking this would solve the problem, but instead it added new errors, i.e. “Unable to resolve resource bundle “collections” for locale “en_US”.

    This is really frustrating as I assumed the HorizontalList would naturally be included in the sdk (actually it is. the compiler just isn’t seeing it) and I’m not seeing any solutions to this problem after hours of googling and testing. Any ideas?

  7. Hey Mister!

    In reference to your ‘Another Solution’ section, I tried this for a collection of images rather than text as per your example, and as you found with rendering the entire list, I also had to wait for the graphics to draw (Spectrum 48K loading style, just a bit quicker!). After a little tinkering though, I found that you can avoid this problem by specifying a rowheight on the component.

    Lastly, thanks for your research and documentation, really helpful. Just surprised that there wasn’t an easier documented way of doing this with Flex 3.5.

  8. First of all, great work on this! Very helpful indeed – thank you. 🙂

    Like Victor (above), I’ve noticed if I add items to the list, the scrollbar gets smaller as the list gets longer. However, if I remove items from the list, the scrollbar doesn’t resize, and the list doesn’t get any shorter (whitespace is left where the removed items used to be)… Any thoughts?

    Thanks
    -Rich

  9. wrt my previous post, I found the problem. Replace:

    combinedRendererWidth += columnWidth;

    with:

    if(renderer!=null)
    combinedRendererWidth += columnWidth;

    Cheers

  10. Thanks much for this info – the first class SmoothHorizontalScrollList works great.

    To hide the scrollbars I set the horizontalScrollBar visible & includeInLayout properties to false. It doesn’t show up anymore, and things look ok for me :

    override protected function configureScrollBars():void
    {
    	super.configureScrollBars();
    	if (horizontalScrollBar) {
    		horizontalScrollBar.includeInLayout=horizontalScrollBar.visible=false;
    		horizontalScrollBar.lineScrollSize = .125;  // should be inverse power of 2
    	}
    }
    

    I changed the mousewheel so that it moves incrementally as well:

    override protected function mouseWheelHandler(event:MouseEvent):void
    {
    	// Scrolldirection and distance calculation
    	if (horizontalScrollBar) {
    		var scrollAmount:Number = horizontalScrollBar.lineScrollSize * event.delta * -1.0;
    
    		var oldPosition:Number = super.horizontalScrollPosition;
    		var newPosition:Number = super.horizontalScrollPosition + scrollAmount;
    
    		if (newPosition  maxHorizontalScrollPosition)
    			newPosition = maxHorizontalScrollPosition;
    
    		horizontalScrollBar.scrollPosition = newPosition;
    
    		var scrollEvent:ScrollEvent = new ScrollEvent(ScrollEvent.SCROLL);
    		scrollEvent.detail = event.delta.toString();
    		scrollEvent.direction = ScrollEventDirection.HORIZONTAL;
    		scrollEvent.position = newPosition;
    		scrollEvent.delta = newPosition - oldPosition;
    
    		horizontalScrollBar.dispatchEvent(scrollEvent);
    	}
    }
    

  11. Rich (and Mister, and Ben),

    Thank you all for this all this great inventiveness and improvements. One more (very slight) one to add to Rich’s mods…

    Allow for switching-off of horizontal scroll bars, via setting horizontal scroll policy off on parent container:

    change:

    // need to shrink list height when canvas has a scrollbar so the scrollbar doesn’t overlap the listheight = ( Container( parent ).horizontalScrollPolicy = ScrollPolicy.OFF ) ? parent.height – 16 : parent.height;

    to:

    // need to shrink list height when canvas has a scrollbar so the scrollbar doesn’t overlap the list
    // added detection to avoid shrink when horizontalScrollPolicy of parent container is set to “off”
    height = ( Container(parent).maxHorizontalScrollPosition > 0 && !Container(parent).horizontalScrollPolicy == ScrollPolicy.OFF ) ? parent.height – 16 : parent.height;

    (the negation of ScrollPolicy.OFF after the [&&] seems to be necessary for an unambiguous detection of scroll policy)

    Thank you gentlemen!

    Richard

  12. Hi i am using the same logic for Image gallery and created Horizontal List. But i am rendering the images dynamically and images get streamed when i give path of image but some images are missing from the Horizontal List when i scroll the Horizontal Bar.

    Kindly help me if u have any idea.

    Thanks

Comments are closed.