Monday, April 23, 2012

GWT Star Rating Widget with Half Stars

One of the rages these days are fancy-star ratings. We see them everywhere from Amazon, Netflix, Google Shopping, and even Wikipedia. 

GWT does not come with a star rating widget built in, but there are some pretty good ones out there already like:
http://code.google.com/p/cobogw/

However they all have full rating images and I wanted something more like Amazon or Netflix with 2.5 or 4.5 stars, or with a little modification 3.75 stars.  So my version is based off of my HorizontalDivPanel class that fixes the table-based layout widgets with a better div-based approach.  So we would first need to get this code and place it in our project:
http://programmingfortherestofus.blogspot.com/2012/04/fixing-gwts-layout-widgets.html

Next find any image you would like to use for your rating image, I would recommend something smal and with an even number of pixels for its width. Now we are going to create 3 versions:
  1. selected: This is the "normal" image that will appear for stars 1 to the rating value.
  2. unselected: This is the default image used, and will appear for the images that a greater than the rating value.
  3. hover: This appears when a user hovers over a rating image.
Then take each of those images and crop them directly in half with the the left and right side added to the filename. Finally we need an image use to clear or reset rating value, like a red or gray X.  We should now have the following file names, which I prefixed with "star_" and placed them my project's war folder "images/starrating" but you could change them to whatever you wish either in the code or using one of the constructors:
  • clear.png
  • star_hover_left.png
  • star_hover_right.png
  • star_selected_left.png
  • star_selected_right.png
  • star_unselected_left.png
  • star_unselected_right.png
Great! Now on to the code. Just like creating any widget we extend Composite, and to get the behavior we want we extend ClickHandler, MouseOverHandler, MouseOutHandler, and also HasValue<Integer> for use in your MVP project.  Next we ad a HorizontalDivPanel (again from a previous blog of mine) to contain the images, and finally an Image uses to clear the rating value when the widget is not set to read only.  The 2 constructors I only use are Star() and Star(boolean read_only).  The read only value determines if the user can change the value of the widget or it is just there to display the rating value.

/**
 * 
 */
package com.yourproject.widgets;

import com.google.gwt.dom.client.Style;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseOverEvent;
import com.google.gwt.event.dom.client.MouseOverHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.Image;

/**
 * @author Austin_Rappa
 *
 */
public class StarRating extends Composite implements ClickHandler, MouseOverHandler, MouseOutHandler, HasValue<Integer> {

    private HorizontalDivPanel mainPanel = new HorizontalDivPanel();
    private Image clearImage = new Image();
    
    private int rating = 0;
    private int rating_max = 10;
    private int hover_index = 0;
    private boolean read_only = false;     
    private String star_selected_left_path = "images/starrating/star_selected_left.png";
    private String star_selected_right_path = "images/starrating/star_selected_right.png";
    private String star_unselected_left_path = "images/starrating/star_unselected_left.png";
    private String star_unselected_right_path = "images/starrating/star_unselected_right.png";
    private String star_hover_left_path = "images/starrating/star_hover_left.png";
    private String star_hover_right_path = "images/starrating/star_hover_right.png";
    private String clear_path = "images/starrating/clear.png";
    
    
    
    /**
     * Star rating widget
     */
    public StarRating() {
        this(0, 10, false);
    }
    
    
    /**
     * Star rating widget
     * @param read_only
     */
    public StarRating(boolean read_only){
        this(0, 10, read_only);
    }
    
    
    /**
     * Star rating widget
     * @param rating
     * @param rating_max
     */
    public StarRating(int rating, int rating_max){
        this(rating, rating_max, false);
    }
    
    
    /**
     * Star rating widget
     * @param rating
     * @param rating_max
     * @param read_only
     */
    public StarRating(int rating, int rating_max, boolean read_only){
        this.setRating(rating);
        this.setRatingMax(rating_max);
        this.setReadOnly(read_only);
        this.buildWidget();
    }
    

    
    
    
    /**
     * Star rating widget
     * @param rating
     * @param rating_max
     * @param read_only
     * @param star_selected_left
     * @param star_selected_right
     * @param star_unselected_left
     * @param star_unselected_right
     * @param star_hover_left
     * @param star_hover_right
     */
    public StarRating(int rating, int rating_max, boolean read_only, String star_selected_left, String star_selected_right, String star_unselected_left, String star_unselected_right, String star_hover_left, String star_hover_right){
        this.setRating(rating);
        this.setRatingMax(rating_max);
        this.setReadOnly(read_only);
        this.setStarSelectedLeft(star_selected_left);
        this.setStarSelectedRight(star_selected_right);
        this.setStarUnselectedLeft(star_unselected_left);
        this.setStarUnselectedRight(star_unselected_right);
        this.setStarHoverLeft(star_hover_left);
        this.setStarHoverRight(star_hover_right);        
        this.buildWidget();
    }
    
    
    
    
    /**
     * Builds the widget
     */
    private void buildWidget() {        
        Image.prefetch(this.getStarSelectedLeftPath());
        Image.prefetch(this.getStarSelectedRightPath());
        Image.prefetch(this.getStarUnselectedLeftPath());
        Image.prefetch(this.getStarUnselectedRightPath());
        Image.prefetch(this.getStarHoverLeftPath());
        Image.prefetch(this.getStarHoverRightPath());
        Image.prefetch(this.getClearPath());
        
        //Initialize
        initWidget(mainPanel); 
        mainPanel.setStyleName("starrating");
        this.addMouseOutHandler(this);
                
        //Stars
        for (int i = 0; i < this.getRatingMax(); i++) {
            Image image = new Image();
            
            //Settings
            image.setStyleName("star");
            image.setTitle("" + (i + 1));
            image.addClickHandler(this);
            image.addMouseOverHandler(this);
            
            mainPanel.add(image);
        }
        
        //If not readonly
        if (!this.isReadOnly()) {
            mainPanel.getElement().getStyle().setCursor(Style.Cursor.POINTER);
            
            //Clear image
            clearImage.setUrl(this.getClearPath());
clearImage.setTitle("clear"); clearImage.addClickHandler(this); clearImage.addMouseOverHandler(this); mainPanel.add(clearImage); } //Set the star images this.setStarImages(); } /** * * @param handler * @return */ private HandlerRegistration addMouseOutHandler(MouseOutHandler handler) { return addDomHandler(handler, MouseOutEvent.getType()); } /** * Resets the button images */ private void setStarImages() { for (int i = 0; i < this.getRatingMax(); i++) { Image image = (Image)mainPanel.getWidget(i); image.setUrl(this.getImagePath(i)); } } /** * Gets the star image based on the index * @param index * @return */ private String getImagePath(int index) { String path = ""; if (index % 2 == 0) { if (index >= this.getHoverIndex()) { if (index >= this.getRating()) { path = this.getStarUnselectedLeftPath(); } else { path = this.getStarSelectedLeftPath(); } } else { path = this.getStarHoverLeftPath(); } } else { if (index >= this.getHoverIndex()) { if (index >= this.getRating()) { path = this.getStarUnselectedRightPath(); } else { path = this.getStarSelectedRightPath(); } } else { path = this.getStarHoverRightPath(); } } return path; } /** * @param rating the rating to set */ public void setRating(int rating) { this.rating = rating; } /** * @return the rating */ public int getRating() { return rating; } /** * @param rating_max the rating_max to set */ public void setRatingMax(int rating_max) { this.rating_max = rating_max; } /** * @return the rating_max */ public int getRatingMax() { return rating_max; } /** * @param hover_index the hover_index to set */ public void setHoverIndex(int hover_index) { this.hover_index = hover_index; } /** * @return the hover_index */ public int getHoverIndex() { return hover_index; } /** * @param read_only the read_only to set */ public void setReadOnly(boolean read_only) { this.read_only = read_only; } /** * @return the read_only */ public boolean isReadOnly() { return read_only; } /** * @param star_selected_left the star_selected_left to set */ public void setStarSelectedLeft(String star_selected_left) { this.star_selected_left_path = star_selected_left; } /** * @return the star_selected_left */ public String getStarSelectedLeftPath() { return star_selected_left_path; } /** * @param star_selected_right the star_selected_right to set */ public void setStarSelectedRight(String star_selected_right) { this.star_selected_right_path = star_selected_right; } /** * @return the star_selected_right */ public String getStarSelectedRightPath() { return star_selected_right_path; } /** * @param star_unselected_left the star_unselected_left to set */ public void setStarUnselectedLeft(String star_unselected_left) { this.star_unselected_left_path = star_unselected_left; } /** * @return the star_unselected_left */ public String getStarUnselectedLeftPath() { return star_unselected_left_path; } /** * @param star_unselected_right the star_unselected_right to set */ public void setStarUnselectedRight(String star_unselected_right) { this.star_unselected_right_path = star_unselected_right; } /** * @return the star_unselected_right */ public String getStarUnselectedRightPath() { return star_unselected_right_path; } /** * @param star_hover_left the star_hover_left to set */ public void setStarHoverLeft(String star_hover_left) { this.star_hover_left_path = star_hover_left; } /** * @return the star_hover_left */ public String getStarHoverLeftPath() { return star_hover_left_path; } /** * @param star_hover_right the star_hover_right to set */ public void setStarHoverRight(String star_hover_right) { this.star_hover_right_path = star_hover_right; } /** * @return the star_hover_right */ public String getStarHoverRightPath() { return star_hover_right_path; } /** * @param clear_path the clear_path to set */ public void setClearPath(String clear_path) { this.clear_path = clear_path; } /** * @return the clear_path */ public String getClearPath() { return clear_path; } /** * On mouse over event */ public void onMouseOver(MouseOverEvent event) { if (!this.isReadOnly()) { Image image = (Image)event.getSource(); if (image.equals(clearImage)) { this.setHoverIndex(0); } else { this.setHoverIndex(Integer.parseInt(image.getTitle())); } this.setStarImages(); } } /** * On click event */ public void onClick(ClickEvent event) { if (!this.isReadOnly()) { Image image = (Image)event.getSource(); if (image.equals(clearImage)) { this.setValue(0, true); } else { this.setValue(Integer.parseInt(image.getTitle()), true); } } } /** * On mouse out event */ public void onMouseOut(MouseOutEvent event) { this.setHoverIndex(0); this.setStarImages(); } /** * Adds a ValueChangehandler */ public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Integer> handler) { return addHandler(handler, ValueChangeEvent.getType()); } /** * Get the rating value */ public Integer getValue() { return this.getRating(); } /** * Set the rating value * @param value the rating to set */ public void setValue(Integer value) { this.setRating(value); this.setStarImages(); } /** * Set the rating value * @param value the rating to set * @param fireEvents fire events */ public void setValue(Integer value, boolean fireEvents) { this.setValue(value); if (fireEvents) ValueChangeEvent.fire(this, value); } }

My first attempt was to do all of the image handling in CSS, because adding handlers to all of the images is just a ton of overhead. But in order to get the hover behavior to change the images from star 1 to whatever star is being hovered on, we sort of have to.  There is a lot to add, like a value to even use 1/4 stars or full stars, and to ensure the rating_max is divisible by that value. But you get the idea.

Now in a your View or other Widget you could instantiate the StarRating like so:

StarRating star1 = new StarRating();
        
StarRating star2 = new StarRating(true);
        
StarRating star3 = new StarRating(5, 10, false);

No comments:

Post a Comment