论坛首页 移动开发技术论坛

Animation and Transition with LWUIT

浏览 3519 次
该帖已经被评为隐藏帖
作者 正文
   发表时间:2010-01-01  

   转载:http://today.java.net/article/2008/11/18/animation-and-transition-lwuit

Animations and transitions are two very interesting features of the Lightweight User Interface Toolkit (LWUIT). In this article we will see how to use these features in actual applications. We also develop a custom transition animation. Although very simple, this transition demonstrates fully the process of such customization.

I have used the Sprint Wireless Toolkit 3.3.1 (Sprint WTK) for the screenshots and also to build the CustomTransitionDemo. I have also referred to the source code for the LWUIT Demo application that comes with the LWUIT download bundle to illustrate the usage of the animation and transition capabilities of LWUIT. For a proper understanding of this article, the toolkit and the LWUIT bundle should be downloaded and installed on your computer. Version 3.3.2 of the Sprint WTK has been released; I have tried it out and found that there is no problem in running our demo on the new version.

Animation

It was possible to animate images on the Java ME platform even before LWUIT appeared on the scene. There are a number of features in the javax.microedition.lcdui.game package that support animation. In addition, the JSR 226 API provides support for SVG-based animation. LWUIT handles animation a little differently, providing support for a capability that is broad-based and simple to use. In the context of LWUIT, animation essentially allows objects to paint succeeding frames at timed intervals. Animation support in LWUIT also makes it easy for transitions to be implemented.

To be capable of being animated, an object has to implement the Animation interface. Animation has two methods.

  • animate: This is a callback method invoked once for every frame.
  • paint: This method is called if animate returns true. If the object being animated is a component, it will have a paint method and that is the one to be called as the signature of the two methods is identical.

The Component class extends Animation. This makes every widget animation-capable. However, in order to actually carry out animation, an object must also be registered for animation by calling the registerAnimated method on its parent form. The animate method of a registered object is called once for every frame of animation as determined by the Display class. If animate returns true, the paint method of the object is called and a repaint takes place. To stop animation, the object must be deregistered by calling the deregisterAnimated method on the parent form.

 

To see how an animation works, let us look at the AnimationDemo class of the LWUITDemo application that comes with the Sprint Toolkit and also with the LWUIT download bundle. The screenshots below show snapshots of two different frames of the animation demo.


Figure 1. Two frames of the animation demo

The demo shows two examples of animation: one uses an animated image of Duke and the other repeatedly draws a set of images to simulate motion. In both cases, the base component that is animated is a button. Other components can also be animated in a similar way. You can replace the buttons in the code for AnimationDemo with labels and the demo will still work fine. Just remember to import the Label class.

In the execute method of AnimationDemo, the first button -- animation -- is instantiated and added to the form (f), which has been passed as a parameter to the method:


        Button animation = new Button(UIDemoMIDlet.
                getResource("duke").getAnimation("duke3_1.gif"));
        animation.setBorderPainted(false);
        animation.getStyle().setBgTransparency(0);
        f.setLayout(new BoxLayout(BoxLayout.Y_AXIS));
        f.addComponent(animation);

The second and third lines are for visual effects only and the fourth sets the layout manager for the form. The interesting thing to note here is that the button has not been registered for animation. This is because the image itself is animated and does not require repainting. The image of Duke used here is an animated gif. At this time LWUIT supports only animated gif format; as a matter of fact, the Resource Editor does not recognize any other animated format.

For the second button, the image array is set up for the paint method:


                Resources images = UIDemoMIDlet.getResource("images");
                Image a = images.getImage("progress0.png");
                Image b = images.getImage("progress1.png");
                Image c = images.getImage("progress2.png");
                final Image[] secondAnimation = new Image[] {
                        a, 
                        b, 
                        c, 
                        a.rotate(90), 
                        b.rotate(90), 
                        c.rotate(90), 
                        a.rotate(180), 
                        b.rotate(180), 
                        c.rotate(180),
                        a.rotate(270), 
                        b.rotate(270), 
                        c.rotate(270),
                };

We now have a set twelve images that are to be drawn sequentially. The code for creating the second button looks like this:


        Button animation2 = new Button() {

                private int currentImage = 0;
                private long lastInvoke;

                public boolean animate() {
                long current = System.currentTimeMillis();
                if (current - lastInvoke > 50) {
                        lastInvoke = current;
                        currentImage++;
                        if (currentImage == secondAnimation.length) {
                                currentImage = 0;
                                }
                                return true;
                        }
                        return false;
                }

                public void paint(Graphics g) {
                        g.drawImage(secondAnimation[currentImage],
                                 getX(), getY());
                }
        };

Here we have a button that implements both the animate and paint methods. The animate method will be called at every frame interval. If more than 50 milliseconds have elapsed since the last call, animate will increment the pointer to the image array and return true; otherwise, it will just return false. If true is returned, then the paint method will be called and the appropriate image will be drawn.

The rest of the code for the second button is mainly for setting the visual parameters and for adding the button to the form. Note the last line of code, which shows that the button has been registered for animation. This ensures that the animate method will be called for every frame.


        animation2.setPreferredSize(new Dimension
                (secondAnimation[0].getWidth(), 
                                secondAnimation[0].getHeight()));
        animation2.setBorderPainted(false);
        animation2.getStyle().setBgTransparency(0);
        f.addComponent(animation2);

        f.registerAnimated(animation2);

And that is all there is to applying animation!

Transitions

The term transition refers to the way a form is brought into (animate in transition) and taken out of (animate out transition) display. The base class for implementing transition is Transition, which implements Animation. This is why transitions work basically as animations and utilize the same callback mechanism as any other animated component. However, Transition is different from Component in the sense that a Component animates its own rendering while Transition controls the animated rendering of forms and dialogs.

There is also a difference between the ways in which component animations and transitions are used. To use transitions, it is not necessary to register the form concerned for animation. When a form transition occurs, the Display object directly places the form into the queue for receiving callbacks.

The Transition class is an abstract one and cannot be instantiated. The LWUIT library includes two concrete subclasses of Transition that can be used to apply transitions. These classes are:

  • CommonTransitions
  • Transition3D

CommonTransitions currently includes two types of transition animations.

  • Slide: The new form slides in, pushing out the current one.
  • Fade: The new form fades in, while the current one fades out.

CommonTransitions works with a supporting class -- Motion -- that provides the models for simulating physical motion. The three types of motions which Motion contains are:

  • Friction
  • Spline
  • Linear

The Transition3D class works with the M3G API (JSR 184) to provide transition animations with 3D effects. JSR 184 support is necessary for these transitions to work properly, and so the Motion class is not required for Transition3D. A current limitation of this class is that its transitions work only with forms, but that is likely to change soon. The 3D suite of transitions contains the following.

  • Cube: Simulates a rotating cube, with the current form on the face rotating off the screen and the new one on the face rotating into the screen.
  • Fly In: The new form flies in.
  • Rotation: A two-dimensional version of Cube in which a plane, rather than a cube, rotates.
  • Static Rotation: Applicable to a transition from a form into a dialog. Only the dialog rotates while the form remains static.
  • Swing In: The incoming form swings into place either from top to bottom or from bottom to top.

Transitions are very easy to use, as we can see from the following code snippets (from the TransitionDemo class of the LWUIT demo application), which are examples of how to create transitions by using the applicable factory methods:


        //creates an out transition
        //with a right-to-left outgoing slide
        out = CommonTransitions.createSlide(
            CommonTransitions.SLIDE_HORIZONTAL, false, runSpeed);

        //creates an out transition
        //with a left-to-right incoming slide
        in = CommonTransitions.createSlide(
            CommonTransitions.SLIDE_HORIZONTAL, true, runSpeed);
        .
        .
        .
        //creates outgoing and incoming fade transitions
        out = CommonTransitions.createFade(runSpeed);
        in = CommonTransitions.createFade(runSpeed);
        .
        .
        .
        //creates outgoing and incoming fly in transitions
        out = Transition3D.createFlyIn(runSpeed);
        in = Transition3D.createFlyIn(runSpeed);


You can see that there is no difference in the way a transition is created, regardless of whether it is a common transition or a 3D transition. After creating a transition, it has to be installed for the desired form. This too is simple:


        //f is the form for which transitions are being set
        f.setTransitionOutAnimator(out);
        f.setTransitionInAnimator(in);

Note that if you set the outgoing transition for a form it is not necessary to set the incoming transition for the new form that is entering the screen; as the old form goes out it is automatically replaced by the new one. When an application has a number of forms, I find it a good practice to set only the outgoing transitions for all the forms. This way I avoid conflicts and missing transitions.

A Custom Transition

One of the great things about LWUIT is that it supports a good deal of customization. So, if you want a transition that is not available in the transition classes that come with the library, you can easily write your own. In this section we develop a simple transition to demonstrate the basic techniques for creating custom transitions. Before we start on our project, though, I would like to add a caveat here. The new transition is only a tool for understanding how transitions and motions work. To keep it simple, I have imposed certain restrictions that make this class unsuitable for general use. Some of these are:

  • It does not support dialogs.
  • It is not configurable and has only one kind of motion -- horizontal, towards the right.
  • It does not check for all conditions that may be encountered in a real device.

Our transition is called Step Transition. As its name suggests, it makes the destination screen move into position in a series of discrete steps, unlike Slide Transition. Another difference between the two transitions is that with step transition, the incoming screen does not appear to push the source screen out -- it moves over the source, gradually covering it.

To create a custom transition, we need to write a class that extends Transition. In this case the class is StepTransition. We also need a model for the appropriate motion and, for our demo, that is the StepMotion class.

We start by looking at StepTransition class. The Transition class has the following abstract methods that have to be implemented by StepTransition:

  • public abstract Transition copy()
  • public abstract boolean animate()
  • public abstract void paint(Graphics g)

In addition, there is the empty initTransition method that will have to be suitably implemented. Depending on the nature of the transition, this method may not be required at all, in which case you may omit it altogether.

The initTransition method is a callback that is invoked at the beginning of each transition. So this is the place to initialize all parameters for starting the transition. We also instantiate a StepMotion object, which will work out the position for rendering the destination screen (remember this is the only screen that moves) for each frame. Our code for initTransition is pretty simple:


        public void initTransition()
        {
                //initialize variables
                //required to set up motion
                Component source = getSource();
                int width = source.getWidth();
                position = 0;
                int startoffset = 0;

                //create a StepMotion object for this transition
                motion = new StepMotion(startoffset,
                        width, duration, step);

                //start the movement for the transition
                motion.start();
        }

Thanks to the simplicity of our specifications, we need to do just a few things to initialize the transition. What we do is initialize the variables required to create the motion object, create the motion, and start it running.

The animate method is called once for every frame. This method checks to see if the motion object is missing or whether the transition is over. The check for completion of transition involves calling the isFinished method of StepMotion, which returns true if the transition is done. If there there is no motion object or if the transition is complete, false is returned so that this transition activity is terminated. Otherwise, the transition will continue and the position for drawing the destination screen corresponding to the next frame is obtained. Finally, animate returns true so the next frame can be drawn:


        public boolean animate()
        {
                //see if there is a motion object
                //and check if the transition is completed
                if(motion == null || motion.isFinished())
                {
                        //in either case terminate transition
                        return false;
                }

                //if transition is to continue
                //get the new position
                position = motion.getStep();

                //continue with transition
                return true;
        }

Since StepTransition implements Animation, it has a paint method that is invoked to refresh the destination screen for each frame. As we see in the code segment above, the position for drawing the destination screen is returned by the getStep method of the StepMotion class. Since the value of position changes only in discrete steps, it may remain unchanged over a number of frames. Drawing the screen(s) for every frame would be a waste of CPU time. To avoid this, getStep returns -1 if the position has remained unchanged since the previous frame. The paint method checks the value of position and calls paintSlideAtPosition to do the actual painting only when necessary. The code for the paint method is given below:


        public void paint(Graphics g)
        {
                //paint only if new position is available
                if(position > -1)
                {
                        paintSlideAtPosition(g, position, 0);
                        return;
                }
                else
                {
                        return;
                }
        }

The actual rendering is done by the paintComponent method of the Component class after the graphics context has been prepared by the following methods:

  • private void paintSlideAtPosition(Graphics g, int slideX, int slideY)
  • private void paint(Graphics g, Component cmp, int x, int y)

For our simple application, we could have combined all the paint methods into one. However, I have retained the structure of CommonTransitions class that comes with the library. I hope this will make it easy to follow the method sequence for anyone who wishes to study the source code.

The paintSlideAtPosition method first checks whether the first form of the application is being displayed and, in that case, inhibits transition. Otherwise it sets up the clip region and, more importantly for this application, the co-ordinates for translation so that proper shifting of the destination screen can occur. The next method takes care of the actual translation, calls the paintComponent on Component to draw the destination screen at the right position, and restores the original translation. The demo, as it stands, causes the destination screen to move over a stationary source. This happens because only the destination screen is repainted for each new position. If you want to achieve a push effect, just uncomment the two bold lines of code in the paintSlideAtPosition method. This will cause the source screen also to be repainted and you will see it moving out of the display area as the destination screen moves in. The two methods are shown below:


        private void paintSlideAtPosition(
                Graphics g, int slideX, int slideY)
        {
                Component source = getSource();

                //if this is the first form being displayed we
                //can't do a step transition
                //since we have no source form
                if (source == null)
                { 
                        return;           
                }

                Component dest = getDestination();
               
                int w = -source.getWidth();                    
                int h = 0;

                //set the clips

                //uncomment the two following lines
                //for the "push" effect
                //g.setClip(source.getAbsoluteX(), 
                        source.getAbsoluteY(), source.getWidth(), 
                        source.getHeight());
                //paint(g, source, slideX , slideY );

                g.clipRect(dest.getAbsoluteX(), 
                        dest.getAbsoluteY(), source.getWidth(), 
                        source.getHeight());
                paint(g, dest, slideX + w, slideY + h);
        
        }

        private void paint(Graphics g, Component cmp, int x, int y)
        {
                //get original clip details
                int cx = g.getClipX();
                int cy = g.getClipY();
                int cw = g.getClipWidth();
                int ch = g.getClipHeight();

                //translate to proper position 
                //for drawing the form
                g.translate(x, y);

                //get it painted
                cmp.paintComponent(g, false);

                //restore original values
                g.translate(-x, -y);
                g.setClip(cx, cy, cw, ch);
    }

The copy method just returns a copy of the step transition object. Note that a copy is returned and not the object itself, as the Display class wants a copy to work with.


        //return a functionally equivalent transition object
        public Transition copy()
        {
                return new StepTransition(duration, step);
        }

The constructor of StepMotion takes four integers as parameters.

  • sourcevalue: This is the starting position. If the new screen starts moving in from the left edge, then this will be zero.
  • destinationvalue: This is the finishing position. If the new screen should stop at the right edge, then this will be equal to the width of the display area.
  • duration: The time period (in milliseconds) over which the transition should last.
  • steps: The number of steps the transition should take.

The constructor calculates the value by which the position of the destination screen has to be incremented from one step to the next. It also calculates the minimum time that has to elapse before the screen can be refreshed.


        public StepMotion(int sourcevalue, int destinationvalue, 
                int duration, int steps)
        {
                this.destinationvalue = destinationvalue;
                this.duration = duration;
                this.steps = steps;

                //the size of a step
                stepsize = (destinationvalue - sourcevalue)/steps;

                //the time interval between two successive steps
                interval = duration/steps;
        }

We have already met the three methods of StepMotion. The only point to note here is that calculated step size may not be a factor of the total distance (destinationvalue - sourcevalue) to be covered as we are using integer math. In such a case, the getStep method will ensure that missing distance is made up as it always takes one step more than calculated number of steps. This is because isFinished returns true only when the number of steps taken (stepnumber) exceeds the required number of steps. This extra step doesn't really cause any problem, as the position is not allowed to exceed destinationvalue. The methods are:

        //save the time as the beginning of an interval
        public void start()
        {
                starttime = System.currentTimeMillis();
        }

        //return true if all steps have been taken care of
        public boolean isFinished()
        {
                return stepnumber > steps;
        }

        //return the position
        public int getStep()
        {
                //check if interval for next step is over
                //if so increment stepnumber 
                //and return (stepsize*stepnumber)
                //also reset starttime by calling start()
                //if (stepsize*stepnumber>destinationvalue) 
                //then return destinationValue
                //if interval not over return -1

                if(System.currentTimeMillis() >= 
                        starttime + interval)
                {
                        stepnumber++;
                        int offset = stepnumber*stepsize;
                        if(offset > destinationvalue)
                        {
                                offset = destinationvalue;
                        }

                        start();

                        return offset;
                }
                else
                {
                        return -1;
                }
        }


The MIDlet that puts it all together is also quite straightforward. After calling init on Display, it initializes the parameters for transition and sets up the theme. It then creates two labels for the two forms that will be used in the demo. We would like the the two forms to look somewhat different so that the transition becomes visually prominent. So the first form is created and its titlebar is made different from the theme setting. The label srclabel is added to the first form, and so are the commands. The MIDlet is then set as the command listener for the form. Finally the first form is made visible:


        //init the LWUIT Display
        Display.init(this);

        //duration of transition in milliseconds
        int duration = 3000;

        //number of steps in transition
        int numofsteps = 10;

        //get the theme and set it
        try
      {
                Resources r = Resources.open("/SDTheme1.res");
                UIManager.getInstance().setThemeProps(
                        r.getTheme("SDTheme1"));
      }
      catch (java.io.IOException e)
      {
      }

        //create two labels
        Label srclabel = new Label("This is the source screen");
        Label dstlabel = new Label(
                "This is the destination screen");

        //create the first form
        first = new Form("Source");

        //we make the titlebar different 
        //from the theme setting
        first.getTitleStyle().setBgImage(null);

        //add srclabel to the first form
        first.addComponent(srclabel);

        //add 'Next' command to first form
        //the command id is 1
        first.addCommand(new Command("Next", 1));

        //add 'Exit' command to first form
        //the command id is 0
        first.addCommand(new Command("Exit", 0));

        //this MIDlet is the listener 
        //for the first form's commands
        first.setCommandListener(this);

        //show the first form
        first.show();

The next part of the code, which deals with the second form, is almost the same. The fundamental difference is that in and out transitions are set for this form. As this demo has only two forms, I have violated my own recommendation and set both transitions for the same form.


        //create the second form
        second = new Form("Destination");

        //set the Out and In transitions for the second form
        second.setTransitionOutAnimator(new StepTransition
                (duration, numofsteps));
        second.setTransitionInAnimator(new StepTransition
                (duration, numofsteps));

        //set Menu and background images for second form
        try
        {
                second.getMenuStyle().setBgImage(Image.createImage
                        ("/pattern3.png"));
                
        }
        catch(java.io.IOException ioe)
        {
        }

        try
        {
                second.getStyle().setBgImage(Image.createImage
                        ("/sdsym4.png"));
        }
        catch(java.io.IOException ioe)
        {
        }

        //add dstlabel to second form
        second.addComponent(dstlabel);

        //add 'Previous' command to first form
        //the command id is 2
        second.addCommand(new Command("Previous", 2));

        //this MIDlet is the listener 
        //for the second form's commands
        second.setCommandListener(this);

All the above action takes place in the startApp method. Now we take care of the commands and we're done:


        public void actionPerformed(ActionEvent ae)
        {
                Command cmd = ae.getCommand();
                switch (cmd.getId())
                {
                        //'Exit' command
                        case 0:
                        notifyDestroyed();
                                break;

                        //'Next' command
                        //show second form
                        case 1:
                                second.show();
                                break;

                        //'Previous' command
                        //show first form
                        case 2:
                                first.show();
                }
        }

The three screenshots in Figures 2 through 4 show progressive images of the step transition from the first form to the second.

Figure 2. Step number 2 of transition

Figure 3. Step number 5 of transition

Figure 4. Step number 8 of transition

Conclusion

We have seen how to use the animation and transition functions of LWUIT and how to create our own transition. LWUIT is an evolving library and we are bound to see additions to its current repertoire. Developers will have to constantly update their knowledge to remain abreast of the new functions, capabilities, and refinements. It's going to need some hard work, but it's going to be great fun too!

Resources

 

Biswajit Sarkar is an electrical engineer with a specialization in programmable industrial automation.
论坛首页 移动开发技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics