javaswtswt-awt

Java SWT : Badge Notifications


I have a desktop based UI application written in Java SWT running on windows.

I want to add a button on the UI screen whose behaviour should be similar to the badges on an iphone or facebook notifications as shown in the images below.

The number on the badge will be dynamic and will increase or decrease based on the number of pending notifications.

How can I implement something similar in SWT/AWT?

IOS Badge:

enter image description here

Facebook Notification:

enter image description here


Solution

  • I've implemented something like that recently. You can simply paint a custom image with GC, and the overlay on your desired icon.

    I'm including my helper class here. It's not the cleanest code (a lot of stuff is hardcoded), but you'll get the point. The notification bubble resizes itself depending on the number of notifications (max 999).

    How to use (Remember to cache and/or dispose your images!):

    Image decoratedIcon = new ImageOverlayer()
            .baseImage(baseImage) // You icon/badget
            .overlayImage(ImageOverlayer.createNotifImage(5)) // 5 notifications 
            .overlayImagePosition(OverlayedImagePosition.TOP_RIGHT)
            .createImage();
    


    /**
     * <pre>
     * The difference between this and the ImageBuilder is 
     * that ImageOverlayer does not chain the images, rather
     * just overlays them one onto another.
     * 
     * 
     * Rules:
     * 
     * 1.) Images are not disposed. Resource handing must be done externally.
     * 2.) Only two images allowed, for now.
     * 3.) The size of the composite image should normally be the size of the
     *     base image, BUT: if the overlaying image is larger, then larger
     *     parameters are grabbed, and the base image is still underneath.
     * 4.) Use the builder APIs to set the base and overlaying images. The 
     *     position of the overlaying image is optional, and CENTER by default.
     *     When you've set these, simply call createImage()
     * 
     * Further improvements:
     * 
     * - Combine this with ImageBuilder. These two composers should be welded.
     * 
     * </pre>
     * 
     * @author grec.georgian@gmail.com
     *
     */
    public class ImageOverlayer extends CompositeImageDescriptor
    {
    
        // ==================== 1. Static Fields ========================
    
        public enum OverlayedImagePosition
        {
            TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT, CENTER;
        }
    
    
        // ====================== 2. Instance Fields =============================
    
        private ImageData baseImageData;
    
        private ImageData overlayedImageData;
    
        private OverlayedImagePosition overlayedImagePosition = OverlayedImagePosition.CENTER;
    
    
    
        // ==================== 3. Static Methods ====================
    
        /**
         * Creates a red circle with a white bold number inside it.
         * Does not cache the final image.
         */
        public static final Image createNotifImage(final int numberOfNotifications)
        {
            // Initial width and height - hardcoded for now
            final int width = 14;
            int height = 14;
    
            // Initial font size
            int fontSize = 100;
    
            int decorationWidth = width;
    
            String textToDraw = String.valueOf(numberOfNotifications);
    
            final int numberLength = Integer.toString(numberOfNotifications).length();
    
            if(numberLength > 3)
            {
                // spetrila, 2014.12.17: - set a width that fits the text
                //                       - smaller height since we will have a rounded rectangle and not a circle
                //                       - smaller font size so the new text will fit(set to 999+) if we have
                //                         a number of notifications with more than 3 digits
                decorationWidth += numberLength * 2;
                height -= 4;
    
                fontSize = 80;
                textToDraw = "999+"; //$NON-NLS-1$
            }
            else if (numberLength > 2)
            {
                // spetrila, 2014.12.17: - set a width that fits the text
                //                       - smaller height since we will have a rounded rectangle and not a circle
                decorationWidth += numberLength * 1.5;
                height -= 4;
            }
    
            final Font font = new Font(Display.getDefault(), "Arial", width / 2, SWT.BOLD); //$NON-NLS-1$
    
            final Image canvas = new Image(null, decorationWidth, height);
    
            final GC gc = new GC(canvas);
    
            gc.setAntialias(SWT.ON);
            gc.setAlpha(0);
            gc.fillRectangle(0, 0, decorationWidth, height);
    
            gc.setAlpha(255);
            gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_RED));
    
            // spetrila, 2014.12.17: In case we have more than two digits in the number of notifications,
            //                       we will change the decoration to a rounded rectangle so it can contain
            //                       all of the digits in the notification number
            if(decorationWidth == width)
                gc.fillOval(0, 0, decorationWidth - 1, height - 1);
            else
                gc.fillRoundRectangle(0, 0, decorationWidth, height, 10, 10);
    
            final FontData fontData = font.getFontData()[0];
            fontData.setHeight((int) (fontData.getHeight() * fontSize / 100.0 + 0.5));
            fontData.setStyle(SWT.BOLD);
    
            final Font newFont = new Font(Display.getCurrent(), fontData);
    
    //      gc.setFont(AEFUIActivator.getDefault().getCustomizedFont(font, fontSize, SWT.BOLD));
            gc.setFont(newFont);
            gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
    
            final Point textSize = gc.stringExtent(textToDraw);
            final int xPos = (decorationWidth - textSize.x) / 2;
            final int yPos = (height - textSize.y) / 2;
            gc.drawText(textToDraw, xPos + 1, yPos, true);
    
            gc.dispose();
    
            final ImageData imgData = canvas.getImageData();
    
            // Remove white transparent pixels
            final int whitePixel = imgData.palette.getPixel(new RGB(255,255,255));
            imgData.transparentPixel = whitePixel;
            final Image finalImage = new Image(null, imgData);
    
            canvas.dispose();
            font.dispose();
            newFont.dispose();
    
            return finalImage;
        }
    
    
        // ==================== 5. Creators ====================
    
        @Override
        public Image createImage()
        {
            if (baseImageData == null || overlayedImageData == null)
                throw new IllegalArgumentException("Please check the ImageOverlayer. One of the overlaying images is NULL."); //$NON-NLS-1$
    
            return super.createImage();
        }
    
    
        // ==================== 6. Action Methods ====================
    
        @Override
        protected void drawCompositeImage(final int width, final int height)
        {
            /*
             * These two determine where the overlayed image top left
             * corner should go, relative to the base image behind it.
             */
            int xPos = 0;
            int yPos = 0;
    
            switch (overlayedImagePosition)
            {
                case TOP_LEFT:
                    break;
    
                case TOP_RIGHT:
                    xPos = baseImageData.width - overlayedImageData.width;
                    break;
    
                case BOTTOM_LEFT:
                    yPos = baseImageData.height - overlayedImageData.height;
                    break;
    
                case BOTTOM_RIGHT:
                    xPos = baseImageData.width - overlayedImageData.width;
                    yPos = baseImageData.height - overlayedImageData.height;
                    break;
    
                case CENTER:
                    xPos = (baseImageData.width - overlayedImageData.width) / 2;
                    yPos = (baseImageData.height - overlayedImageData.height) / 2;
                    break;
    
                default:
                    break;
            }
    
            drawImage(baseImageData, 0, 0);
            drawImage(overlayedImageData, xPos, yPos);
        }
    
    
        // ==================== 7. Getters & Setters ====================
    
        final public ImageOverlayer overlayImagePosition(final OverlayedImagePosition overlayImagePosition)
        {
            this.overlayedImagePosition = overlayImagePosition;
            return this;
        }
    
    
        final public ImageOverlayer baseImage(final ImageData baseImageData)
        {
            this.baseImageData = baseImageData;
            return this;
        }
    
    
        final public ImageOverlayer baseImage(final Image baseImage)
        {
            this.baseImageData = baseImage.getImageData();
            return this;
        }
    
    
        final public ImageOverlayer baseImage(final ImageDescriptor baseImageDescriptor)
        {
            this.baseImageData = baseImageDescriptor.getImageData();
            return this;
        }
    
    
        final public ImageOverlayer overlayImage(final ImageData overlayImageData)
        {
            this.overlayedImageData = overlayImageData;
            return this;
        }
    
    
        final public ImageOverlayer overlayImage(final Image overlayImage)
        {
            this.overlayedImageData = overlayImage.getImageData();
            return this;
        }
    
    
        final public ImageOverlayer overlayImage(final ImageDescriptor overlayImageDescriptor)
        {
            this.overlayedImageData = overlayImageDescriptor.getImageData();
            return this;
        }
    
    
        @Override
        protected Point getSize()
        {
            // The size of the composite image is determined by the maximum size between the two building images,
            // although keep in mind that the base image always comes underneath the overlaying one.
            return new Point( max(baseImageData.width, overlayedImageData.width), max(baseImageData.height, overlayedImageData.height) );
        }
    
    }