Welcome!

Welcome to the official BlackBerry Support Community Forums.

This is your resource to discuss support topics with your peers, and learn from each other.

inside custom component

Java Development

Implementing a standard style scrollbar on a Blackberry device

by Developer on ‎07-22-2010 11:47 AM - edited on ‎09-20-2010 04:59 PM by BlackBerry Development Advisor (17,181 Views)

Problem: Standard BlackBerry scrollbar leaves much to be desired.


A standard BlackBerry scrollbar (shown, for example, by a VerticalFieldManager with VERTICAL_SCROLL | VERTICAL_SCROLLBAR style) does not give the BlackBerry® device user any idea of how big is the screen height nor what part of the whole screen is above and below the visible area.  It just displays a small triangle in the bottom right corner when there is more data to be viewed below the visible window and another one in the top right corner when there is more above the screen.  Dry and not very informative.

 

A standard style scrollbar with a slider height giving the user some rough idea about the relationship between the visible area and the whole document and a slider position indicating what percentage of the whole document is below (and above) the currently displayed part would be much nicer and "professional-looking".

Some useful math first: A standard VerticalFieldManager (as well as any net.rim.device.api.ui.Manager) provides access to some useful bits of information:

Total Manager's height: Manager.getVirtualHeight()
Manager's visible height: Manager.getVisibleHeight()
Current scroll position: Manager.getVerticalScroll()

This information is sufficient to calculate the size and the position of the scrollbar slider:

slider height / visible height = visible height / total height gives us slider height = visible height ^ 2 / total height
slider position = scroll position * visible height / total height (on the screen; don't forget that in the associated Graphics object all positions are relative to the top left corner of the whole Manager)

Making room for the scrollbar on the right: in order to lay out the managed fields properly, we are going to invoke super.sublayout().  However, we need to make some room for our own scrollbar to be displayed on top of that.  How?  Just steal a little of the whole width before passing it to the superclass, then restore it back so that we can later draw in that area.  The managed fields will not know about that little extra space we've created, so they will never paint there!

What class to extend? VerticalFieldManager.java looks like the most natural candidate.  If we want finer control over the layout, we might want to extend Manager directly and then implement the whole sublayout() with layoutChild() and setPositionChild() calls.  Such an approach, however, is well beyond the scope of this article and is thus left as an exercise for the reader.
A note of caution: if you use a MainScreen (or extend it) and just add this VerticalScrollManager to it, you will not see any good-looking scrollbars, but rather the default Blackberry device one.  How come?  The default MainScreen comes with a VERTICAL_SCROLL | VERTICAL_SCROLLBAR VerticalFieldManager as its delegate.  Such a manager gives all the height you want (up to 0x3FFFFFFF) to the managed fields.  Thus, the underlying VerticalScrollManager will think it's got enough room to display all fields at once.  Use MainScreen(NO_VERTICAL_SCROLL | NO_VERTICAL_SCROLLBAR) as a container.  Alternatively, take a look at the various constructors of VerticalScrollManager below: some let you restrict the maximum dimensions of the Manager.  This way you can have several of such scrolling "windows" on the screen, share the display with other fields/Managers, etc.

 

A few more details: I learned the hard way that adding this Manager as its own ScrollChangeListener and calling invalidate() in its scrollChanged() method is necessary - touch-screen devices with gesture scrolling can behave weird otherwise.  Also, painting the scroll slider with lighter left and top borders and darker right and bottom ones makes it look 3-D so I added this.  Remove the code after setting the TOP_SHADE_COLOR and BOTTOM_SHADE_COLOR (four drawLine calls) to make it flat.

 

Let's use all of the above to implement a nice-looking scrollbar:
VerticalScrollManager.java:
import net.rim.device.api.ui.container.VerticalFieldManag er;
import net.rim.device.api.ui.Graphics;
import net.rim.device.api.ui.Manager;
import net.rim.device.api.ui.ScrollChangeListener;
/**
* VerticalScrollManager - a VerticalFieldManager with a nicer-looking scrollbar
* and optional width and height limits
*/
public class VerticalScrollManager extends VerticalFieldManager implements ScrollChangeListener {
/**
* Some constants governing the exact look - colors and size - of the scrollbar.
*
* Our scrollbar is a light grey bar on the right of the manager's visible area
* with a darker grey scroll slider.
*/
private static final int SCROLLBAR_COLOR = 0xCCCCCC;
private static final int SLIDER_COLOR = 0x66666666;
private static final int TOP_SHADE_COLOR = 0xDDDDDD;
private static final int BOTTOM_SHADE_COLOR = 0x333333;
private static final int SCROLLBAR_WIDTH = 8;
private static final int SCROLLBAR_RIGHT_MARGIN = 0;
private static final int SCROLLBAR_LEFT_MARGIN = 0;

// The eventual height of the slider in pixels
private int sliderHeight;
// The eventual horizontal slider position - in this Manager's coordinates
private int sliderXPosition;
// Height and width limits - useful for creating Managers which should not take
// the whole provided area
private int maxVisibleHeight;
private int maxVisibleWidth;
// Actual height and width - set in sublayout() below
private int visibleHeight;
private int visibleWidth;
// Total (a.k.a "virtual") height of the Manager
private int totalHeight;
// Do we need to display a scrollbar?
private boolean isScrolling;

// Constructors - please observe the use of width and height limits
// Also - you do want to use VERTICAL_SCROLL but definitely not the
// default "scrollbar", thus we made that a default
public VerticalScrollManager() {
this(VERTICAL_SCROLL | NO_VERTICAL_SCROLLBAR, Integer.MAX_VALUE, Integer.MAX_VALUE);
}

public VerticalScrollManager(int w, int h) {
this(VERTICAL_SCROLL | NO_VERTICAL_SCROLLBAR, w, h);
}

public VerticalScrollManager(long style) {
this(style, Integer.MAX_VALUE, Integer.MAX_VALUE);
}

public VerticalScrollManager(long style, int w, int h) {
super(style);
maxVisibleHeight = h;
maxVisibleWidth = w;
setScrollListener(this);
}

// This is how we report our desided height and width to the parent manager
public int getPreferredHeight() {
return visibleHeight;
}

public int getPreferredWidth() {
return visibleWidth;
}

// This is called by the framework just before displaying this Manager. At this point we are
// given the biggest rectangle within the parent Manager that our Manager is allowed to occupy
// This is the natural place to make all necessary calculations
protected void sublayout(int w, int h) {
// Initial value - no scrollbar unless VERTICAL_SCROLL is requested
isScrolling = ((getStyle() & VERTICAL_SCROLL) == VERTICAL_SCROLL);
// How much room (horizontally) do we need for the scrollbar
int scrollbarWidth = isScrolling ? SCROLLBAR_WIDTH + SCROLLBAR_LEFT_MARGIN + SCROLLBAR_RIGHT_MARGIN : 0;
// Further limit the given dimensions with the requested size
visibleHeight = Math.min(h, maxVisibleHeight);
visibleWidth = Math.min(w, maxVisibleWidth);
// Before asking the parent class to layout, reserve the necessary room for the scrollbar
int myWidth = visibleWidth - scrollbarWidth;
super.sublayout(myWidth, visibleHeight);
// After the VerticalFieldManager lays out its fields, let's ask it for dimensions and
// adjust width back to include the scrollbar
visibleHeight = getHeight();
totalHeight = getVirtualHeight();
visibleWidth = getWidth() + scrollbarWidth;
// Report our proper dimensions to the parent Manager
setExtent(visibleWidth, visibleHeight);
// This is necessary for the overall BlackBerry framework to know how far we can scroll
// Especially important for touch-screen devices
setVirtualExtent(visibleWidth, totalHeight);
// Now, let's double check whether any scrollbar is needed
// If the visible area is tall enough, let's not bother
isScrolling = (visibleHeight < totalHeight);

// Finally, determine how big is the slider and where to start painting it horizontally
if (isScrolling) {
sliderHeight = visibleHeight * visibleHeight / totalHeight;
sliderHeight = Math.max(sliderHeight, 1); // show at least one pixel!
// Please observe that we reserved the width based on both left and right margin,
// but are going to paint based on right margin only - that's how we create
// that left margin
sliderXPosition = visibleWidth - SCROLLBAR_WIDTH - SCROLLBAR_RIGHT_MARGIN;
}
}

// This is called each time our Manager needs repainting (invalidate(), scrolling, etc.)
protected void paint(Graphics g) {
// First, paint the fields "normally"
super.paint(g);

// Now, add the scrollbar if necessary
if (isScrolling) {
// Determine how far have we scrolled
int scrollPosition = getVerticalScroll();
// The slider vertical position on the screen is proportional to the scroll position.
// Please observe that we add the scroll position to the calculated result since
// everything on the screen starts there. All x and y coordinates for this Graphics
// object are within the Manager's FULL (virtual) rectangle.
int sliderYPosition = scrollPosition * visibleHeight / totalHeight + scrollPosition;
// draw the scrollbar
g.setColor(SCROLLBAR_COLOR);
// Again, scrollbar starts at scroll position (top of the displayed part) and
// is visibleHeight high
g.fillRect(sliderXPosition, scrollPosition, SCROLLBAR_WIDTH, visibleHeight);
// draw the slider
g.setColor(SLIDER_COLOR);
g.fillRect(sliderXPosition, sliderYPosition, SCROLLBAR_WIDTH, sliderHeight);
// draw the shading - make it "3-D"
g.setColor(TOP_SHADE_COLOR);
if (sliderHeight > 2) {
g.drawLine(sliderXPosition, sliderYPosition, sliderXPosition + SCROLLBAR_WIDTH - 1, sliderYPosition);
}
g.drawLine(sliderXPosition, sliderYPosition, sliderXPosition, sliderYPosition + sliderHeight - 1);

g.setColor(BOTTOM_SHADE_COLOR);
if (sliderHeight > 2) {
g.drawLine(sliderXPosition, sliderYPosition + sliderHeight - 1, sliderXPosition + SCROLLBAR_WIDTH - 1, sliderYPosition + sliderHeight - 1);
}
g.drawLine(sliderXPosition + SCROLLBAR_WIDTH - 1, sliderYPosition, sliderXPosition + SCROLLBAR_WIDTH - 1, sliderYPosition + sliderHeight - 1);
}
}

public void scrollChanged(Manager mgr, int newX, int newY) {
if (mgr == this) {
invalidate(newX + sliderXPosition, newY, SCROLLBAR_WIDTH + SCROLLBAR_RIGHT_MARGIN, getVisibleHeight());
}
}
}


Comments
by Developer on ‎08-17-2010 12:10 PM

In fact, scrollChanged can be modified to invalidate only the scrollbar area:

    public void scrollChanged(Manager mgr, int newX, int newY) {
if (mgr == this) {
invalidate(newX + sliderXPosition, newY, SCROLLBAR_WIDTH + SCROLLBAR_RIGHT_MARGIN, getVisibleHeight());
}
}

 

 

by Administrator on ‎08-17-2010 01:11 PM

Thanks arkadyz.  I'll have the article updated with the new scrollChanged method.

by Developer on ‎10-25-2010 05:31 PM

Here is variation on the above, which displays the scroll bar over the top of the screen, then fades it out (mostly).  Based on code supplied during the DEV17 session at Devcon, but with a few additions to help it cope with horizontal scrolling screens, simplify the interpolation (not now as slick, but a lot easier to understand) and cope better with a screen that constantly gets updated (e.g. by menu), meaning the scrollbar should be redisplayed. 

 

Hopefully arkadyz will see his way clear to reviewing this and applying his skill at explaining it, for bears with simple brains like me. 

 

Until then, enjoy this yourself.  Just use this in place of VerticalFieldManager and see what happens....

 

/**
 * VerticalOverlayScrollbarManager.java
 *
 * Developed from the VerticalOverlayScrollbarManager provided by RIM at DEVCON 2010, DEV17
 * in combination with the ideas presented in the sample VSM presented here
 * http://supportforums.blackberry.com/t5/Java-Development/Implementing-a-standard-style-scrollbar-on-a...
 */

import net.rim.device.api.system.Application;
import net.rim.device.api.ui.Graphics;
import net.rim.device.api.ui.Manager;
import net.rim.device.api.ui.ScrollChangeListener;
import net.rim.device.api.ui.container.VerticalFieldManager;
import net.rim.device.api.util.MathUtilities;

public class VerticalOverlayScrollbarManager extends VerticalFieldManager implements ScrollChangeListener {

    private static final int FADE_DELAY = 2000; // Delay till fade starts
    private static final int FADE_DURATION = 2000; // Delay after FADE_DELAY till fade completes
    private static final int FADE_REFRESH = Math.max(30, FADE_DURATION/10); // Millisecs between refreshes, 10 refreshes over duration...
    private static final int MAX_FADE_VALUE = 100; // percent when fade starts
    private static final int MIN_FADE_VALUE = 15;  // percent when fade completes
   
    private static final int SCROLLBAR_COLOR = 0x0;
    private static final int SCROLLBAR_WIDTH = 5;
    private static final int SCROLLBAR_ALPHA = 0x60;
    private static final int SCROLLBAR_MIN_HEIGHT = 20; //pixels

    private SimpleInterpolater _fadeInterpolator = new SimpleInterpolater(MIN_FADE_VALUE, MAX_FADE_VALUE);

    private InvalidateRunnable _invalidateRunnable = new InvalidateRunnable();
    private FadeRunnable _fadeRunnable = new FadeRunnable();
   
    private Application _application;
   
    public VerticalOverlayScrollbarManager() {
        this( 0 );
    }
   
    public VerticalOverlayScrollbarManager( long style ) {
        super( style | Manager.VERTICAL_SCROLL | Manager.NO_VERTICAL_SCROLLBAR );
        setScrollListener( this );
    }
   
    protected boolean isScrollCopyable() {
        return false;
    }

    protected void onDisplay() {
        super.onDisplay();
        _application = Application.getApplication();
        // Force a scroll changed to get initial fade
        scrollChanged(this, this.getHorizontalScroll(), this.getVerticalScroll());
    }

    protected void onExposed() {
        super.onExposed();
        _application = Application.getApplication();
        // Force a scroll changed to get initial fade
        scrollChanged(this, this.getHorizontalScroll(), this.getVerticalScroll());
    }

    protected void onObscured() {
        super.onObscured();
        _application = null; // So we stop updating
    }

    protected void onUndisplay() {
        super.onUndisplay();
        _application = null; // So we stop updating
    }

    public void scrollChanged( Manager manager, int newHorizontalScroll, int newVerticalScroll ) {
        // start a fade request when scroll position changes
        _fadeInterpolator.set( _fadeInterpolator.getMax() );
        _fadeRunnable.startFade();
    }

    protected void paint( Graphics graphics ) {
        super.paint( graphics );
        drawScrollbar( graphics );
       
        if( !_fadeInterpolator.isDone() ) {
            invalidateLater();
        }
    }

    private void invalidateLater() {
        _invalidateRunnable.invoke();
    }

    private void drawScrollbar( Graphics graphics ) {
        int fadeValue = _fadeInterpolator.getCurrentValue();
        int height = getHeight();
        int width  = getWidth();
        int virtualHeight  = getVirtualHeight();
        int verticalScroll = getVerticalScroll();
        int horizontalScroll = getHorizontalScroll();
        if( virtualHeight > height ) {
            // calculate the scroll bar height & position
            int scrollbarHeight = Math.max( SCROLLBAR_MIN_HEIGHT, height * height / virtualHeight );
            int xOffset = horizontalScroll + width - SCROLLBAR_WIDTH - 1;
            int yOffset = verticalScroll + ( height - scrollbarHeight ) * verticalScroll / ( virtualHeight - height );

            int oldColor = graphics.getColor();
            int oldAlpha = graphics.getGlobalAlpha();

            try {
                graphics.setGlobalAlpha( SCROLLBAR_ALPHA * fadeValue / _fadeInterpolator.getMax() );
                graphics.setColor( SCROLLBAR_COLOR );
   
                // draw the scroll with the specified alpha and color
                graphics.fillRoundRect( xOffset, yOffset, SCROLLBAR_WIDTH, scrollbarHeight, 4, 4 );
                graphics.drawRoundRect( xOffset, yOffset, SCROLLBAR_WIDTH, scrollbarHeight, 4, 4 );
            } finally {
                graphics.setGlobalAlpha( oldAlpha );
                graphics.setColor( oldColor );
            }
        }
    }

    private class FadeRunnable implements Runnable {
        private int _id = -1;
        public void run() {
            _id = -1;
            if( _application != null ) {
                _fadeInterpolator.kickTo( FADE_DURATION, _fadeInterpolator.getMin() );
                invalidateLater();
            }
        }
        public void startFade() {
            if( _application == null ) {
                _id = -1;
                return;
            }
            // cancel the current fade request if any
            if( _id != -1 ) {
                _application.cancelInvokeLater( _id );
            }
            // start a new fade request after the delay period
            _id = _application.invokeLater( this, FADE_DELAY, false );
        }
    }        
   
    private class InvalidateRunnable implements Runnable {
        private boolean _pending;
        public void run() {
            _pending = false;
            if( _application != null ) {
                invalidate();
            }
        }
        public void invoke() {
            if( _application == null ) {
                _pending = false;
                return;
            }
            if ( !_pending ) {
                _pending = true;
                _application.invokeLater( this, FADE_REFRESH, false );
            }
        }
    }

}

class SimpleInterpolater extends Object {       
    int _anchor;
    int _target;
    int _distance;
    int _duration;
    long _kickTime;

    int _min = Integer.MIN_VALUE;
    int _max = Integer.MAX_VALUE;

    public SimpleInterpolater( int min, int max ) {
        setBounds( min, max );
    }

    public int getMin() {
        return _min;
    }

    public int getMax() {
        return _max;
    }

    public void setBounds( int min, int max ) {
        _min = min;
        _max = max;
    }

    public boolean outOfBounds( int value ) {
        return value < _min || value > _max;
    }

    private int clamp( int position ) {
        return MathUtilities.clamp( _min, position, _max );
    }

    public void set( int value ) {
        _anchor = _target = clamp( value );
        _distance = 0;
    }

    public void kickTo( int time, int position ) {
        _anchor = getCurrentValue();
        _target = clamp(position);
        _distance = _target - _anchor;
        _duration = time;
        _kickTime = System.currentTimeMillis();
    }

    public int distance() {
        return _distance;
    }

    public int getCurrentValue() {
        if ( _kickTime <= 0 ) {
            return _anchor;
        }
        long t = System.currentTimeMillis() - _kickTime;
        if( t >= _duration ) {
            return _target;
        }
        int fadeValue = _anchor + ((int) (( t * (long)_distance ) / _duration ) );
        return clamp(fadeValue);
    }

    public boolean isDone() {
        return ( System.currentTimeMillis() - _kickTime ) > _duration;
    }

}

 

 

by Developer on ‎11-03-2010 12:02 PM

Hi, Peter,

Thanks for posting the code.

For those who wonder which one suits their application best:

 

1) The code in the article is the bare minimum.  It was created to demonstrate the overall approach to such tasks, not to be a "be-all, end-all" solution.

2) The scrollbar in VerticalOverlayScrollbarManager in semi-transparent and thus does not need a separate room.  The rest of your Fields will be laid out naturally. I consider this a huge improvement. Notice no sublayout() override there.

3) I would still override the sublayout() - call super.sublayout() with the default parameters and then calculate the height of the slider like it is done in the article.  This saves you the same calculation in paint() which, due to all those invalidate() calls during scrolling and fading, executes quite frequently.

4) Notice how we invalidate() only the scrollbar region in the article. I would import this technique to the VerticalOverlayScrollbarManager to save even more CPU cycles. See the first comment.

5) In the article, I went with rectangular 3-D looking scrollbar just for kicks.  I experimented enough with that to learn that a narrow rounded rectangle there looks much nicer and that's exactly what you get in VerticalOverlayScrollbarManager.

 

Compare the two classes, understand the differences and experiment!

Users Online
Currently online: 29 members 1,256 guests
Please welcome our newest community members: