CustomCategoryBar.java

/**
 * Copyright (c) 2013 Nokia Corporation.
 */

package com.nokia.example.picasaviewer.ui;

import java.io.IOException;

import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;

import com.nokia.mid.ui.CanvasGraphicsItem;
import com.nokia.mid.ui.CategoryBar;
import com.nokia.mid.ui.ElementListener;
import com.nokia.mid.ui.IconCommand;

import com.nokia.example.picasaviewer.util.ImageUtils;

/**
 * A custom category bar.
 */
public class CustomCategoryBar
    extends CategoryBar
{
    // Constants
    public static final int HEIGHT = 44;
    private static final int WIDTH = 240;
    private static final int UNDEFINED = -1;

    // Members
    private CustomCategoryBarRenderer _renderer = null;
    private Canvas _canvas = null;
    private ElementListener _listener = null;
    private int _tabCount = 0;
    private int _y = 0;
    private int _pressedIndex = UNDEFINED;
    private boolean _setSelectedIndexAutomatically = false;
    private boolean _notifyIndexChangedOnlyWhenReleased = true;

    /**
     * Creates a new custom category bar instance.
     * @param canvas The canvas which renders the category bar. Can be null if
     * the bar is painted manually.
     * @param unselectedIcons The unselected icons.
     * @return A newly created instance.
     */
    public static CustomCategoryBar getNewInstance(Canvas canvas,
                                                   Image[] unselectedIcons)
    {
        Image[] selectedIcons = createSelectedIcons(unselectedIcons);
        String[] labels = new String[unselectedIcons.length];
        
        for (int i = 0; i < labels.length; ++i) {
            labels[i] = new String();
        }
        
        return new CustomCategoryBar(canvas, unselectedIcons, selectedIcons, labels);
    }

    /**
     * Constructor.
     */
    private CustomCategoryBar(Canvas canvas,
                              Image[] unselectedIcons,
                              Image[] selectedIcons,
                              String[] labels)
    {
        super(unselectedIcons, selectedIcons, labels, CategoryBar.ELEMENT_MODE_STAY_SELECTED);
        _canvas = canvas;
        _tabCount = unselectedIcons.length;
        _renderer = new CustomCategoryBarRenderer(WIDTH, HEIGHT, unselectedIcons, selectedIcons);
    }

    /**
     * @see com.nokia.mid.ui.CategoryBar#setVisibility(boolean)
     */
    public void setVisibility(boolean visible) {
        _renderer._isVisible = visible;
        
        if (_canvas != null) {
            _renderer.setVisible(visible);
        }
    }

    /**
     * @see com.nokia.mid.ui.CategoryBar#getVisibility()
     */
    public boolean getVisibility() {
        return _renderer._isVisible;
    }

    /**
     * @see com.nokia.mid.ui.CategoryBar#setElementListener(com.nokia.mid.ui.ElementListener)
     */
    public void setElementListener(ElementListener listener) {
        super.setElementListener(listener);
        _listener = listener;
    }

    /**
     * @see com.nokia.mid.ui.CategoryBar#setSelectedIndex(int)
     */
    public void setSelectedIndex(int index) {
        super.setSelectedIndex(index);
        _renderer.repaint();
    }

    //-------------------------------------------------------------------------
    // New methods ->

    /**
     * Sets the opacity of the category bar.
     * @param opacity The opacity to set.
     */
    public void setOpacity(float opacity) {
        if (opacity >= 0 && opacity <= 1) {
            _renderer.setOpacity(opacity, true);
        }
    }

    /** 
     * @param set If true will call setSelectedIndex() automatically when
     * notifying the listener.
     */
    public void setSelectedIndexAutomatically(boolean set) {
        _setSelectedIndexAutomatically = set;
    }

    /** 
     * @param notify If true will notify the listener of index change only when
     * the index changes with pointerReleased event. If false, will notify even
     * with pointerPressed and pointerDragged events.
     */
    public void setNotifyIndexChangedOnlyWhenReleased(boolean notify) {
        _notifyIndexChangedOnlyWhenReleased = notify;
    }

    public void onPointerPressed(int x, int y) {
        if (y > _y && y < _y + HEIGHT) {
            final int index = x / _renderer._tabWidth;
            
            if (getSelectedIndex() != index) {
                if (_notifyIndexChangedOnlyWhenReleased) {
                    _pressedIndex = index;
                }
                else {
                    notifyIndexChanged(index);
                }
            }
            
            if (_renderer._setOpacity < 1 && _renderer._actualOpacity != 1.0f) {
                _renderer.setOpacity(1.0f, false);
            }
        }
    }

    public void onPointerDragged(int x, int y) {
        if (y > _y && y < _y + HEIGHT) {
            final int index = x / _renderer._tabWidth;
            
            if (getSelectedIndex() != index
                && !_notifyIndexChangedOnlyWhenReleased)
            {
                notifyIndexChanged(index);
            }
            
            if (_notifyIndexChangedOnlyWhenReleased) {
                _pressedIndex = index;
            }
            
            if (_renderer._setOpacity < 1 && _renderer._actualOpacity != 1.0f) {
                _renderer.setOpacity(1.0f, false);
            }
        }
        else {
            if (_renderer._actualOpacity != _renderer._setOpacity) {
                _renderer.setOpacity(_renderer._setOpacity, false);
            }
            
            _pressedIndex = UNDEFINED;
        }
    }

    public void onPointerReleased(int x, int y) {
        if (y > _y && y < _y + HEIGHT) {
            final int index = x / _renderer._tabWidth;
            
            if (getSelectedIndex() != index) {
                notifyIndexChanged(index);
            }
        }
        
        if (_renderer._setOpacity < 1
            && _renderer._actualOpacity != _renderer._setOpacity)
        {
            _renderer.setOpacity(_renderer._setOpacity, false);
        }
        
        _pressedIndex = UNDEFINED;
    }

    /**
     * Paints the category bar.
     * @param graphics The Graphics instance.
     * @param y The Y coordinate of the top-left corner.
     */
    public void paint(Graphics graphics, int y) {
        if (_canvas == null && _y != y) {
            _y = y;
        }
        
        _renderer.paint(graphics, _y);
    }

    /**
     * @see com.nokia.mid.ui.CategoryBar#setSelectedIndex(int)
     */
    private void notifyIndexChanged(int index) {
        if (_setSelectedIndexAutomatically) {
            setSelectedIndex(index);
        }
        
        if (_listener != null) {
            _listener.notifyElementSelected(this, index);
        }
    }

    /**
     * Creates the selected icons based on the unselected ones.
     * @param unselectedIcons The unselected icons.
     * @return The newly created selected icons image assets.
     */
    private static Image[] createSelectedIcons(Image[] unselectedIcons) {
        final int length = unselectedIcons.length;
        Image[] selectedIcons = new Image[length];
        
        for (int i = 0; i < length; ++i) {
            // Highlight color is 0x29a7cc (RGB: 41, 167, 204)
            selectedIcons[i] = ImageUtils.substractRgb(unselectedIcons[i], 214, 88, 51);
        }
        
        return selectedIcons;
    }

    // <- New methods
    //-------------------------------------------------------------------------

    /* Visibility of the constructors hidden since we do not want to allow to
     * create the factory from the outside
     */
    protected CustomCategoryBar(IconCommand[] elements, boolean useLongLabel) {
        super(elements, useLongLabel);
    }
    protected CustomCategoryBar(IconCommand[] elements, boolean useLongLabel, int mode) {
        super(elements, useLongLabel, mode);
    }
    protected CustomCategoryBar(Image[] unselectedIcons, Image[] selectedIcons, String[] labels) {
        super(unselectedIcons, selectedIcons, labels);
    }
    protected CustomCategoryBar(Image[] unselectedIcons, Image[] selectedIcons, String[] labels, int mode) {
        super(unselectedIcons, selectedIcons, labels, mode);
    }


    /**
     * For rendering our custom category bar.
     */
    private class CustomCategoryBarRenderer
        extends CanvasGraphicsItem
    {
        // Constants
        private final String[] IMAGE_URIS = {
            "/selected-tab-left-corner-edge.png",
            "/selected-tab-left-edge.png",
            "/selected-tab-right-corner-edge.png",
            "/selected-tab-right-edge.png",
            "/selected-tab-texture.png",
            "/tab-left-corner-edge.png",
            "/tab-right-corner-edge.png",
            "/tab-texture.png"
        };
        
        private static final int SELECTED_TAB_LEFT_CORNER_EDGE = 0;
        private static final int SELECTED_TAB_LEFT_EDGE = 1;
        private static final int SELECTED_TAB_RIGHT_CORNER_EDGE = 2;
        private static final int SELECTED_TAB_RIGHT_EDGE = 3;
        private static final int SELECTED_TAB_TEXTURE = 4;
        private static final int TAB_LEFT_CORNER_EDGE = 5;
        private static final int TAB_RIGHT_CORNER_EDGE = 6;
        private static final int TAB_TEXTURE_FULL = 7;
        private static final int TAB_TEXTURE_WITH_ONE_EDGE = 8;
        private static final int TAB_TEXTURE_WITH_TWO_EDGES = 9;
        private static final int IMAGE_ASSET_COUNT = 10;
        
        // Members 
        private Image _opaqueImages[] = null;
        private Image _translucentImages[] = null;
        private Image _currentImages[] = null;
        private Image _unselectedIcons[] = null;
        private Image _selectedIcons[] = null;
        private Image _translucentIcons[] = null;
        private Image _currentIcons[] = null;
        private float _setOpacity = 1.0f;
        private float _actualOpacity = _setOpacity;
        private float _storedOpacity = UNDEFINED;
        private int _edgeImageWidth = 0;
        private int _selectedTabWidthWithoutEdges = 0;
        private int _tabWidth = 0;
        private boolean _isVisible = false;

        /**
         * Constructor.
         * @param width
         * @param height
         * @param unselectedIcons
         * @param selectedIcons
         */
        private CustomCategoryBarRenderer(int width,
                                          int height,
                                          Image[] unselectedIcons,
                                          Image[] selectedIcons)
        {
            super(width, height);
            _unselectedIcons = unselectedIcons;
            _selectedIcons = selectedIcons;
            _currentIcons = unselectedIcons;
            createImages();
            
            if (_canvas != null) {
                setParent(_canvas);
                _y = _canvas.getHeight() - HEIGHT;
                setPosition(0, _y);
            }
        }

        /**
         * Sets the opacity of the category bar. Calling this method has no cost
         * if the opacity below value 1 was set previously as the translucent
         * image assets are already created. However, if the opacity value
         * differs from the previous translucent value, the image assets need to
         * be re-created.
         * @param opacity The opacity to set.
         * @param stick If true, will consider the given opacity non-temporary.
         */
        public void setOpacity(final float opacity, final boolean stick) {
            if (opacity < 0 || opacity > 1) {
                // Invalid opacity
            	return;
            }
            
            if (opacity == 1.0f) {
                System.out.println("CustomCategoryBarRenderer.setOpacity(): Setting to opaque.");
                _currentImages = _opaqueImages;
                _currentIcons = _unselectedIcons;
            }
            else if (_storedOpacity != opacity) {
                System.out.println("CustomCategoryBarRenderer.setOpacity(): New opacity: " + opacity);
                createAndTakeInUseTranslucentImages();
            }
            else {
                // Use the existing assets
                System.out.println("CustomCategoryBarRenderer.setOpacity(): Using stored opacity: " + _setOpacity);
                _currentImages = _translucentImages;
                _currentIcons = _translucentIcons;
            }
            
            _actualOpacity = opacity;
            
            if (stick) {
                _setOpacity = opacity;
            }
            
            repaint();
        }

        /**
         * @see com.nokia.mid.ui.CanvasGraphicsItem#paint(javax.microedition.lcdui.Graphics)
         */
        protected void paint(Graphics graphics) {
            paint(graphics, 0);
        }

        /**
         * For convenience.
         * @param graphics The Graphics instance.
         * @param y The Y coordinate of the bar.
         */
        protected void paint(Graphics graphics, final int y) {
            if (!_isVisible) {
                return;
            }
            
            final int selectedIndex = getSelectedIndex();
            int x = 0;
            final int anchor = Graphics.TOP | Graphics.LEFT;
            
            // Paint the tab bar
            for (int i = 0; i < _tabCount; ++i) {
                if (i == selectedIndex || i == _pressedIndex) {
                    if (i == 0) {
                        graphics.drawImage(_currentImages[SELECTED_TAB_LEFT_CORNER_EDGE], x, y, anchor);
                    }
                    else {
                        graphics.drawImage(_currentImages[SELECTED_TAB_LEFT_EDGE], x, y, anchor);
                    }
                    
                    x += _edgeImageWidth;
                    graphics.drawImage(_currentImages[SELECTED_TAB_TEXTURE], x, y, anchor);
                    x += _selectedTabWidthWithoutEdges;
                    
                    if (i == _tabCount - 1) {
                        graphics.drawImage(_currentImages[SELECTED_TAB_RIGHT_CORNER_EDGE], x, y, anchor);
                    }
                    else {
                        graphics.drawImage(_currentImages[SELECTED_TAB_RIGHT_EDGE], x, y, anchor);
                    }
                    
                    x += _edgeImageWidth;
                }
                else {
                    if (i == 0) {
                        graphics.drawImage(_currentImages[TAB_LEFT_CORNER_EDGE], x, y, anchor);
                        x += _edgeImageWidth;
                    }
                    
                    if (i == 0 && i != _tabCount - 1) {
                        graphics.drawImage(_currentImages[TAB_TEXTURE_WITH_ONE_EDGE], x, y, anchor);
                        x += _tabWidth - _edgeImageWidth;
                    }
                    else if (i == 0 && i == _tabCount - 1) {
                        graphics.drawImage(_currentImages[TAB_TEXTURE_WITH_TWO_EDGES], x, y, anchor);
                        x += _tabWidth - _edgeImageWidth * 2;
                    }
                    else {
                        graphics.drawImage(_currentImages[TAB_TEXTURE_FULL], x, y, anchor);
                        x += _tabWidth;
                    }
                    
                    if (i == _tabCount - 1) {
                        graphics.drawImage(_currentImages[TAB_RIGHT_CORNER_EDGE], x, y, anchor);
                        x += _edgeImageWidth;
                    }
                }
            }
            
            // Paint the icons
            x = 0;
            Image iconImage = null;
            
            for (int i = 0; i < _tabCount; ++i) {
                if (i == selectedIndex || i == _pressedIndex) {
                    iconImage = _selectedIcons[i];
                }
                else {
                    iconImage = _currentIcons[i];
                }
                
                if (iconImage != null) {
                    graphics.drawImage(iconImage,
                        x + (_tabWidth - iconImage.getWidth()) / 2,
                        y + (HEIGHT - iconImage.getHeight()) / 2, anchor);
                }
                
                x += _tabWidth;
            }
        }

        /**
         * Creates the image assets used by the category bar.
         */
        private void createImages() {
            _opaqueImages = new Image[IMAGE_ASSET_COUNT];
            
            try {
                for (int i = 0; i < IMAGE_URIS.length; ++i) {
                    _opaqueImages[i] = Image.createImage(IMAGE_URIS[i]);
                }
            }
            catch (IOException e) {
                System.out.println("CustomCategoryBarRenderer.createImages(): Failed to load image!");
            }
            
            _tabWidth = WIDTH / _tabCount;
            _edgeImageWidth = _opaqueImages[SELECTED_TAB_LEFT_CORNER_EDGE].getWidth();
            _selectedTabWidthWithoutEdges = _tabWidth - _edgeImageWidth * 2; 
            
            Image temp = _opaqueImages[TAB_TEXTURE_FULL];
            _opaqueImages[TAB_TEXTURE_FULL] = ImageUtils.scale(temp, _tabWidth, HEIGHT);
            _opaqueImages[TAB_TEXTURE_WITH_ONE_EDGE] =
                    ImageUtils.scale(temp, _tabWidth - _edgeImageWidth, HEIGHT);
            _opaqueImages[TAB_TEXTURE_WITH_TWO_EDGES] =
                    ImageUtils.scale(temp, _tabWidth - _edgeImageWidth * 2, HEIGHT);
            _opaqueImages[SELECTED_TAB_TEXTURE] =
                ImageUtils.scale(_opaqueImages[SELECTED_TAB_TEXTURE],
                    _selectedTabWidthWithoutEdges, HEIGHT);
            
            _currentImages = _opaqueImages;
        }

        /**
         * Creates the translucent image assets based on the original ones.
         * Note that this is done asynchronously.
         */
        private void createAndTakeInUseTranslucentImages() {
            final float opacity = _setOpacity;
            final int alpha = (int)(255 * opacity);
            
            if (_translucentImages == null) {
                _translucentImages = new Image[_opaqueImages.length];
                _translucentIcons = new Image[_unselectedIcons.length];
            }
            
            new Thread() {
                public void run() {
                    for (int i = 0; i < _opaqueImages.length; ++i) {
                        _translucentImages[i] = ImageUtils.setAlpha(_opaqueImages[i], alpha);
                    }
                    
                    for (int i = 0; i < _unselectedIcons.length; ++i) {
                        _translucentIcons[i] = ImageUtils.setAlpha(_unselectedIcons[i], alpha);
                    }
                    
                    _currentImages = _translucentImages;
                    _currentIcons = _translucentIcons;
                    _storedOpacity = opacity;
                    repaint();
                }
            }.start();
        }
    }
}