I'd like to have a path transition in such a way, that the path behaves relative to the window size, even when the size is changed during the animation. Furthermore, when the animation is completed, the animated element should use the stay at the relative destination location.
tileWidthProperty and tileHeightProperty are class members that I added for convinience and they simply divide the main pane's size in eights.
This is the code I have:
public void applyMove(final Ellipse toBeMoved, final int startCol, final int startRow, final int endCol, final int endRow)
{
Platform.runLater(() ->
{
final Path path = new Path();
final MoveTo startingPoint = new MoveTo();
final LineTo endPoint = new LineTo();
startingPoint.xProperty().bind(tileWidthProperty.multiply(startCol).add(tileWidthProperty.divide(2)));
startingPoint.yProperty().bind(tileHeightProperty.multiply(startRow).add(tileHeightProperty.divide(2)));
endPoint.xProperty().bind(tileWidthProperty.multiply(endCol).add(tileWidthProperty.divide(2)));
endPoint.yProperty().bind(tileHeightProperty.multiply(endRow).add(tileHeightProperty.divide(2)));
path.getElements().add(startingPoint);
path.getElements().add(endPoint);
toBeMoved.centerXProperty().unbind();
toBeMoved.centerYProperty().unbind();
PathTransition transition = new PathTransition(Duration.millis(10000), path, toBeMoved);
transition.setOrientation(PathTransition.OrientationType.NONE);
transition.setCycleCount(1);
transition.setAutoReverse(false);
//bind the node at the destination.
transition.setOnFinished(event ->
{
toBeMoved.centerXProperty().bind(tileWidthProperty.multiply(endCol).add(tileWidthProperty.divide(2)));
toBeMoved.centerYProperty().bind(tileHeightProperty.multiply(endRow).add(tileHeightProperty.divide(2)));
toBeMoved.setTranslateX(0.0);
toBeMoved.setTranslateY(0.0);
});
transition.play();
});
}
The col and row parameters are integers that are known to be 0 <= x < 8. They are the column and row of the tile position in the 8*8 grid.
The binding of the MoveTo and LineTo elements, however does not seem to have any effect.
First of all, once any standard Animation is started it does not change parameters.
So if anything changes, you have to stop the animation and start it again with new parameters.
I noticed you're trying to bind centerX and centerY after the transition is complete, which is wrong: PathTransition moves elements using translateX and translateY.
And for better debug you can actually add Path to the scene graph to see where your element will go.
Path path = new Path();
parent.getChildren().add(path);
I assume that tileWidthProperty and tileHeightProperty are bound to the actual parent size using something like this:
tileWidthProperty.bind(parent.widthProperty().divide(8));
tileHeightProperty.bind(parent.heightProperty().divide(8));
So I created example just to show how it might look like.
import javafx.animation.PathTransition;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Ellipse;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Main extends Application {
private Pane parent;
private final DoubleProperty
tileWidthProperty = new SimpleDoubleProperty(),
tileHeightProperty = new SimpleDoubleProperty();
private EllipseTransition ellipseTransition;
#Override
public void start(Stage primaryStage) {
this.parent = new Pane();
tileWidthProperty.bind(parent.widthProperty().divide(8));
tileHeightProperty.bind(parent.heightProperty().divide(8));
// create ellipse
final Ellipse ellipse = new Ellipse(25., 25.);
parent.getChildren().add(ellipse);
// show the stage
primaryStage.setScene(new Scene(parent, 800, 800));
primaryStage.show();
// create listeners that listen to size changes
InvalidationListener sizeChangeListener = l -> {
if(ellipseTransition != null) {
// refreshAndStart returns null if transition is completed
// let's call it delayed cleanup :)
ellipseTransition = ellipseTransition.refreshAndStart();
} else {
System.out.println("ellipseTransition cleaned up!");
}
};
// add listeners to the corresponding properties
tileWidthProperty.addListener(sizeChangeListener);
tileHeightProperty.addListener(sizeChangeListener);
// move ellipse 0,0 -> 7,7
applyMove(ellipse, 0, 0, 7, 7, Duration.millis(5000));
// interrupt transition at the middle, just for fun
/*new Thread(() -> {
try {
Thread.sleep(2500);
} catch (InterruptedException e) {
return;
}
applyMove(ellipse, 2, 3, 4, 5, Duration.millis(1000));
}).start();*/
}
public void applyMove(final Ellipse toBeMoved, final int startCol, final int startRow, final int endCol, final int endRow, final Duration duration) {
Platform.runLater(() -> {
// if transition is still up, then stop it
if(ellipseTransition != null) {
ellipseTransition.finish();
}
// and create a new one
ellipseTransition = new EllipseTransition(toBeMoved, startCol, startRow, endCol, endRow, Duration.ZERO, duration, 0);
// then start it
ellipseTransition.start();
});
}
// I decided to write separate class for the transition to make it more convenient
private class EllipseTransition {
// these variables are the same you used in your code
private final Path path;
private final Ellipse ellipse; // this one was "toBeMoved"
private final int startCol, startRow, endCol, endRow;
private final Duration duration;
private final PathTransition transition;
// if we change parent size in the middle of the transition, this will give the new transition information about where we were.
private Duration startTime;
// we call System.currentTimeMillis() when we start
private long startTimestamp;
// if true, transition would not start again
private boolean finished;
public EllipseTransition(Ellipse ellipse, int startCol, int startRow, int endCol, int endRow, Duration startTime, Duration duration, long realStartTimestamp) {
this.path = new Path();
this.ellipse = ellipse;
this.startCol = startCol;
this.startRow = startRow;
this.endCol = endCol;
this.endRow = endRow;
this.startTime = startTime;
this.duration = duration;
this.transition = new PathTransition();
// applyMove passes 0, because we don't know our start time yet
this.startTimestamp = realStartTimestamp;
}
// this is called right before starting the transition
private void init() {
// show path for debugging
parent.getChildren().add(path);
// binding values here is useless, you can compute everything in old-fashioned way for better readability
final MoveTo startingPoint = new MoveTo();
startingPoint.setX(tileWidthProperty.get() * startCol + tileWidthProperty.get() / 2.);
startingPoint.setY(tileHeightProperty.get() * startRow + tileHeightProperty.get() / 2.);
final LineTo endPoint = new LineTo();
endPoint.setX(tileWidthProperty.get() * endCol + tileWidthProperty.get() / 2);
endPoint.setY(tileHeightProperty.get() * endRow + tileHeightProperty.get() / 2);
path.getElements().clear(); // clear paths from the last time
path.getElements().add(startingPoint);
path.getElements().add(endPoint);
ellipse.translateXProperty().unbind();
ellipse.translateYProperty().unbind();
transition.setNode(ellipse);
transition.setDuration(duration);
transition.setPath(path);
transition.setOrientation(PathTransition.OrientationType.NONE);
transition.setCycleCount(1);
transition.setAutoReverse(false);
transition.setOnFinished(event ->
{
// bind ellipse to the new location
ellipse.translateXProperty().bind(tileWidthProperty.multiply(endCol).add(tileWidthProperty.divide(2)));
ellipse.translateYProperty().bind(tileHeightProperty.multiply(endRow).add(tileHeightProperty.divide(2)));
// cleanup
stop();
// mark as finished
finished = true;
});
}
// stops the transition
private void stop() {
// remove debug path ( added it in init() )
parent.getChildren().remove(path);
transition.stop();
}
// starts the transition
public void start() {
if(finished) {
return;
}
init(); // initialize parameters
// start from the place where previous we stopped last time
// if we did not stop anywhere, then we start from beginning (applyMove passes Duration.ZERO)
this.transition.playFrom(startTime);
// applyMove passes 0, as it doesn't know when transition will start
// but now we know
if(this.startTimestamp == 0) {
this.startTimestamp = System.currentTimeMillis();
}
}
// stops the transition
public void finish() {
stop();
finished = true;
}
// stops and refreshes the transition.
// that will continue transition but for new values
private void refresh() {
// stop the transition
stop();
// determine how much time we spend after transition has started
long currentDuration = System.currentTimeMillis() - startTimestamp;
// update startTime to the current time.
// when we call start() next time, transition will continue, but with new parameters
this.startTime = Duration.millis(currentDuration);
}
// this method is called from change listener
public EllipseTransition refreshAndStart() {
if(finished) {
// return null to the listener
// we want to cleanup completely
return null;
}
// refresh new values and start
refresh(); start();
return this;
}
}
public static void main(String[] args) {
launch(args);
}
}
Related
I'm new to unity and c#, I have a code to increase energy after 10m, I have used PlayerPrefs to do so it is working in unity but not working on mobile please help.
I'm unable to understand what exactly is causing problem in phone. Also the timer pauses when the app is not killed but just minimized on mobile. I want the timer countdown to keep going game minimized and also if killed.
using System;
using UnityEngine;
using UnityEngine.UI;
public class EnergyAdder : MonoBehaviour
{
// Start is called before the first frame update
public float waitTime = 600;
Timer timer;
public GameRoot gameRoot;
public int count;
DateTime currentDate;
DateTime oldDate;
void Start()
{
//timer = new Timer(waitTime);
float remainingTime = PlayerPrefs.GetFloat("TimeOnExit");
timer = new Timer(remainingTime);
if (PlayerPrefs.HasKey("sysString"))
{
currentDate = DateTime.Now;
long temp = Convert.ToInt64(PlayerPrefs.GetString("sysString"));
oldDate = DateTime.FromBinary(temp);
TimeSpan difference = currentDate.Subtract(oldDate);
count = (int)(difference.TotalSeconds / waitTime);
Debug.Log("time remaining " + count);
gameRoot.AddEnergy(count);
timer = new Timer(remainingTime - (float)difference.TotalSeconds);
//PlayerPrefs.DeleteKey("TimeOnExit");
//PlayerPrefs.DeleteKey("sysString");
}
}
// Update is called once per frame
void Update()
{
if (Gameroot.isFull)
{
GetComponent<Text>().text = "Full";
count = 0;
timer.refresh();
}
else
{
//Debug.Log("deltatime ************************"+ secondstime);
timer.countDown();
if (timer.isFinished())
{
timer = new Timer(waitTime);
timer.refresh();
gameRoot.AddEnergy(1);
}
UpdateClock();
}
}
void UpdateClock()
{
int seconds = ((int)timer.timeLeft) % 60;
int minutes = (int)(timer.timeLeft / 60);
GetComponent<Text>().text = minutes.ToString() + ":" + seconds.ToString("00");
}
void OnApplicationQuit()
{
PlayerPrefs.SetFloat("TimeOnExit", timer.timeLeft);
var today = DateTime.Now;
PlayerPrefs.SetString("sysString", today.ToBinary().ToString());
PlayerPrefs.Save();
}
}
Please help what is wrong in above code or what is missing.
From OnApplicationQuit
Note: iOS applications are usually suspended and do not quit. You should tick "Exit on Suspend" in Player settings for iOS builds to cause the game to quit and not suspend, otherwise you may not see this call. If "Exit on Suspend" is not ticked then you will see calls to OnApplicationPause instead.
I suspect it is just the same with modern Android where your app is also not really closed but hibernate.
So I think you could use OnApplicationPause and OnApplicationFocus
On Android, when the on-screen keyboard is enabled, it causes a OnApplicationFocus(false) event. Additionally, if you press "Home" at the moment the keyboard is enabled, the OnApplicationFocus() event is not called, but OnApplicationPause() is called instead.
and just do the same as you do in OnApplicationQuit also for OnApplicationPause(true) and OnApplicationFocus(false)
and do the same as you do in Start also in OnApplicationPause(false) and OnApplictionFocus(true) like
public class EnergyAdder : MonoBehaviour
{
public float waitTime = 600;
public GameRoot gameRoot;
public int count;
// already reference this in the Inspector
[SerializeField] private Text timerText;
private Timer timer;
private DateTime currentDate;
private DateTime oldDate;
private void Awake()
{
// store this reference ONCE
if(!timerText) timerText = GetComponent<Text>();
}
private void Start()
{
GetTime();
}
private void Update()
{
// Note: probably a typo? shouldn't it be "gameRoot" ?
if (Gameroot.isFull)
{
timerText.text = "Full";
count = 0;
timer.refresh();
}
else
{
//Debug.Log("deltatime ************************"+ secondstime);
timer.countDown();
if (timer.isFinished())
{
timer = new Timer(waitTime);
timer.refresh();
gameRoot.AddEnergy(1);
}
UpdateClock();
}
}
private void UpdateClock()
{
int seconds = ((int)timer.timeLeft) % 60;
int minutes = (int)(timer.timeLeft / 60);
timerText.text = $"{minutes}:{seconds:00}";
}
private void OnApplicationQuit()
{
StoreTime();
}
// pauseStatus is True if the application is paused, else False.
private void OnApplicationPause(bool pauseStatus)
{
if(pauseStatus)
{
StoreTime();
}
else
{
GetTime();
}
}
// hasFocus is True if the GameObjects have focus, else False.
private void OnApplicationFocus(bool hasFocus)
{
if(hasFocus)
{
GetTime();
}
else
{
StoreTime();
}
}
private void StoreTime()
{
PlayerPrefs.SetFloat("TimeOnExit", timer.timeLeft);
var today = DateTime.Now;
PlayerPrefs.SetString("sysString", today.ToBinary().ToString());
PlayerPrefs.Save();
}
private void GetTime()
{
// NOTE: You should consider to pass a default value like
//float remainingTime = PlayerPrefs.GetFloat("TimeOnExit", 15);
float remainingTime = PlayerPrefs.GetFloat("TimeOnExit");
timer = new Timer(remainingTime);
if (PlayerPrefs.HasKey("sysString"))
{
currentDate = DateTime.Now;
long temp = Convert.ToInt64(PlayerPrefs.GetString("sysString"));
oldDate = DateTime.FromBinary(temp);
TimeSpan difference = currentDate.Subtract(oldDate);
count = (int)(difference.TotalSeconds / waitTime);
Debug.Log("time remaining " + count);
gameRoot.AddEnergy(count);
timer = new Timer(remainingTime - (float)difference.TotalSeconds);
}
}
}
Note: Typed on smartphone so I can't test it but I hope it goes into the right direction
A little introduction: I've created a simple (for now) application which uses an AnimationTimer to update animations and draw objects to the Canvas. Everything runs smoothly and the timer adjusts its fps to the refresh-rate of my laptop (50/60Hz).
When I start the program however, there seems to be something wrong which causes my animations to appear 'jurky' or dropping frames, but the framerate stays a solid 60/50fps. Then when I resize the window for the first time (no difference how many), suddenly all the animations are super-smooth. After that, everything stays 'synced' no matter the resizes done.
What is the reason that the AnimationTimerstarts 'out-of-sync' until the window is resized and can it be prevented?
Update
Added a code example. The problem is mostly visible when on 50Hz, but also exist on 60Hz. Using Eclipse on Windows 10 (first code-share, may be to much/missing things).
public void start(Stage primaryStage) {
try {
pane = new Pane();
drawables = new ArrayList<>();
canvas = new Canvas(400,400);
canvas.widthProperty().bind(pane.widthProperty());
canvas.heightProperty().bind(pane.heightProperty());
GraphicsContext g = canvas.getGraphicsContext2D();
SimpleAnimatedCircle circle = new SimpleAnimatedCircle(20);
circle.setX(100);
circle.setY(50);
timer = new AnimationTimer() {
#Override
public void handle(long now) {
frameCount++;
if (System.currentTimeMillis() > frameStart + 500) {
System.out.println("FPS: " + frameCount*2);
frameStart = System.currentTimeMillis();
frameCount = 0;
}
for (Drawable drawable:drawables) {
drawable.update();
}
g.setFill(Color.DARKSLATEBLUE);
g.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
circle.draw(g);
}
};
timer.start();
canvas.setOnMouseClicked((e) -> {
circle.start();
});
pane.getChildren().add(canvas);
Scene scene = new Scene(pane,400,400);
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static class SimpleAnimatedCircle {
double diameter;
double x;
double y;
long startTime;
double diffY = 300; // Animated distance over y-axis.
double duration = 2000; // 2 second duration.
public SimpleAnimatedCircle(double diameter) {
this.diameter = diameter;
}
public void setX(double value) {
x = value;
}
public void setY(double value) {
y = value;
}
public void start() {
startTime = System.currentTimeMillis();
}
public void draw(GraphicsContext g) {
double animatedY = y;
// Update the animation.
if (System.currentTimeMillis() < startTime + duration) {
animatedY = y + (System.currentTimeMillis() - startTime) /
duration * diffY;
}
g.setFill(Color.ORANGE);
g.fillOval(x, animatedY, diameter, diameter);
}
}
Yesterday as I was working on my game and I decided I wanted to include some sort of a video showing sneak peaks of the themes and tutorials and such. I then decided to check how to record my scene or nodes. After looking around i concluded there was no easy way to do it so I decided to make my own utility which I want to share with all of you.
Please know I am more or less a beginner and I havent been coding for long. I know it is probably not properly done and that it can be done in much better ways. Anyways here it goes.
/*
* JavaFX SceneCaptureUtility 2016/07/02
*
* The author of this software "Eudy Contreras" grants you ("Licensee")
* a non-exclusive, royalty free, license to use,modify and redistribute this
* software in source and binary code form.
*
* Please be aware that this software is simply part of a personal test
* and may in fact be unstable. The software in its current state is not
* considered a finished product and has plenty of room for improvement and
* changes due to the range of different approaches which can be used to
* achieved the desired result of the software.
*
* BEWARE that because of the nature of this software and because of the way
* this software functions the ability of this software to be able to operate
* without probable malfunction is strictly based on factors such as the amount
* of processing power the system running the software has, and the resolution of
* the screen being recorded. The amount of nodes on the scene will have an impact
* as well as the size and recording rate to which this software will be subjected
* to. IN CASE OF MEMORY RELATED PROBLEMS SUCH AS BUT NOT LIMITEd TO LACK OF REMAINING
* HEAP SPACE PLEASE CONSIDER LOWERING THE RESOLUTION OF THE SCENE BEING RECORDED.
*
* BEWARE STABILITY ISSUES MAY ARISE!
* BEWARE SAVING AND LOADING THE RECORDED VIDEO MAY TAKE TIME DEPENDING ON YOUR SYSTEM
*
* PLEASE keep track of the console for useful information and feedback
*/
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import javax.imageio.ImageIO;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.stage.Screen;
import javafx.util.Duration;
* #author Eudy Contreras
*
* This program records a javaFX scene by taking continuous snapshots of the scene
* which are than stored and saved to a predefined destination. The program allows the
* user to save and retrieve the frame based videos make by this program which can
* then be play on a specified layer or "screen".
public class SceneCaptureUtility {
private int frame = 0; // The current frame which is being displayed
private int timer = 0; // recording timer.
private int record_time; // Amount of time the recorder will record
private int counter = 0;
private int width;
private int height;
private float frameRate = 60.0f;
private long capture_rate = (long) (1000f / frameRate); // Rate at which the recorder will recored. Default rate: 60FPS
private PlaybackSettings playSettings;
private Double frameCap = 1.0 / frameRate; // Framerate at which the recorded video will play
private Double video_size_scale = 1.0; // Scale of the video relative to its size: 0.5 = half, 2.0 = double the size
private Double bounds_scale_X = 0.5; // Scale factor for scaling relative to assigned or obtained resolution
private Double bounds_scale_Y = 0.5;
private Boolean saveFrames = false; //If true saves the individual frames of the video as images
private Boolean loadFrames = false; //If true allows retrieving previously saved frames for playback
private Boolean allowRecording = false;
private Boolean allowPlayback = false;
private Boolean showIndicators = false;
private Pane indicator_layer;
private Pane video_screen;
private Scene scene;
private Timeline videoPlayer;
private ArrayList<Image> recorded_frames; //Stores recorded frames
private ArrayList<ImageView> video_frames; //Stores frames for playback
private ArrayList<byte[]> temp_frames; //Stores frames for saving
private final SnapshotParameters parameters = new SnapshotParameters();
private final ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
private final Indicator recording = new Indicator(Color.RED, " Recording..");
private final Indicator playing = new Indicator(Color.GREEN, " Playing..");
private final Indicator idle = new Indicator(Color.YELLOW, "paused..");
private final String VIDEO_NAME = "recording4.FXVideo";
private final String FRAME_NAME = "image";
private final String DIRECTORY_NAME = "Snake Game Videos"+ File.separator;
private final String PATH_ROOT = System.getProperty("user.home") + "/Desktop" + File.separator +DIRECTORY_NAME;
private final String FILE_EXTENSION = "jpg";
private final String PATH_FRAME = PATH_ROOT + FRAME_NAME;
private final String PATH_VIDEO = PATH_ROOT + VIDEO_NAME;
/**
* Constructs a scene capture utility with a default scene, a pane which
* will be used to diplay the state indicators, the amount of time which
* the recorder will be running and a condition to whether or not the indicators
* will be shown.
* #param scene: scene which will be recored.
* #param indicatorLayer: layer which will be used to show the state indicators.
* #param record_time: time in minutes for which the recorder will be recording
* #param showIndicators: condition which determines if the indicators will be shown.
*/
public SceneCaptureUtility(Scene scene, Pane indicatorLayer, int record_time, boolean showIndicators) {
this.scene = scene;
this.width = (int) scene.getWidth();
this.height = (int) scene.getHeight();
this.showIndicators = showIndicators;
this.record_time = record_time * 60;
this.initStorages(indicatorLayer);
this.loadRecording();
this.scaleResolution(0, 0, false);
}
/*
* Initializes the list used to store the captured frames.
*/
private void initStorages(Pane layer) {
if(showIndicators)
this.indicator_layer = layer;
video_frames = new ArrayList<ImageView>();
recorded_frames = new ArrayList<Image>();
temp_frames = new ArrayList<byte[]>();
}
/**
* loads recordings and or frames from a specified location
*/
private void loadRecording(){
if (loadFrames) {
loadFromFile();
} else {
retrieveRecording();
}
}
/*
* Resets the list
*/
private void resetStorage() {
if (video_frames != null)
video_frames.clear();
if (recorded_frames != null)
recorded_frames.clear();
if (video_screen != null)
video_screen.getChildren().clear();
}
/**
* Method which when called will start recording the given scene.
*/
public void startRecorder() {
if (!allowRecording) {
resetStorage();
if(showIndicators)
showIndicator(indicator_layer.getChildren(), recording);
videoRecorder();
allowRecording(true);
logState("Recording...");
}
}
/**
* Method which when called will stop the recording
*/
public void stopRecorder() {
if (allowRecording) {
if(showIndicators)
showIndicator(indicator_layer.getChildren(), idle);
allowRecording(false);
logState("Recording stopped");
logState("Amount of recorded frames: " + recorded_frames.size());
processVideo();
saveVideo();
}
}
/**
* Method which when called will start playback of the recorded video onto
* a given screen or layer.
* #param output_screen: layer used to display the video
* #param settings: video settings that determine the playback conditions.
*/
public void starPlayer(Pane output_screen, PlaybackSettings settings) {
video_screen = output_screen;
playSettings = settings;
if(showIndicators)
showIndicator(indicator_layer.getChildren(), playing);
if (video_frames.size() > 0) {
logState("Video playback..");
resetPlayback();
if (videoPlayer == null)
videoPlayer();
else {
videoPlayer.play();
}
allowPlayback(true);
}
else{
logState("Nothing to play!");
}
}
/**
* Method which when called will stop the playback of the video
*/
public void stopPlayer() {
if(showIndicators)
showIndicator(indicator_layer.getChildren(), idle);
if (videoPlayer != null)
videoPlayer.stop();
logState("Playback stopped");
allowPlayback(false);
}
/*
* Method which creates a task which records the video at
* a specified rate for a specifed time
*/
private void videoRecorder() {
Task<Void> task = new Task<Void>() {
#Override
public Void call() throws Exception {
while (true) {
Platform.runLater(new Runnable() {
#Override
public void run() {
if (allowRecording && record_time > 0) {
recorded_frames.add(create_frame());
}
recordingTimer();
}
});
Thread.sleep(capture_rate);
}
}
};
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
}
/*
* Method which creates a timeline which plays the video
* at a specified frameRate onto a given screen or layer.
*/
private void videoPlayer() {
videoPlayer = new Timeline();
videoPlayer.setCycleCount(Animation.INDEFINITE);
KeyFrame keyFrame = new KeyFrame(Duration.seconds(frameCap),
new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent e) {
if (allowPlayback) {
playbackVideo();
}
}
});
videoPlayer.getKeyFrames().add(keyFrame);
videoPlayer.play();
}
**/**
* Calls to this method will decreased the time left on the
* recording every second until the recording time reaches
* zero. this will cause the recording to stop.
*/**
private void recordingTimer() {
timer++;
if (allowRecording && timer >= frameRate) {
record_time -= 1;
timer = 0;
if (record_time <= 0) {
record_time = 0;
}
}
}
/**
* A call to this method will add the recorded frames to the video list
* making them reading for playback.
*/
private void processVideo() {
logState("Processing video...");
Task<Void> task = new Task<Void>() {
#Override
public Void call() throws Exception {
for (int i = 0; i < recorded_frames.size(); i++) {
video_frames.add(new ImageView(recorded_frames.get(i)));
}
logState("Video has been processed.");
return null;
}
};
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
}
/**
* Call to this method will play the video on the given screen
* adding a removing frames.
* #return: screen in which the frames are being rendered
*/
private final Pane playbackVideo() {
if (video_screen.getChildren().size() > 0)
video_screen.getChildren().remove(0);
video_screen.getChildren().add(video_frames.get(frame));
frame += 1;
if (frame > video_frames.size() - 1) {
if(playSettings == PlaybackSettings.CONTINUOUS_REPLAY){
frame = 0;
}
else if(playSettings == PlaybackSettings.PLAY_ONCE){
frame = video_frames.size() - 1;
allowPlayback = false;
}
}
return video_screen;
}
public void setVideoScale(double scale) {
this.video_size_scale = scale;
}
/**
* A called to this method will scale the video
* to a given scale.
* #param scale: new scale of the video. 1.0 is normal
* 0.5 is half and 2.0 is twice the size.
*/
public void scaleVideo(double scale) {
this.video_size_scale = scale;
Task<Void> task = new Task<Void>() {
#Override
public Void call() throws Exception {
if (video_frames.size() > 0) {
logState("Scaling video...");
for (int i = 0; i < video_frames.size(); i++) {
video_frames.get(i).setFitWidth(video_frames.get(i).getImage().getWidth() * video_size_scale);
video_frames.get(i).setFitHeight(video_frames.get(i).getImage().getHeight() * video_size_scale);
}
logState("Video has been scaled!");
}
return null;
}
};
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
}
/**
* A called to this method will attempt to prepare the video and or frames
* for saving
*/
private void saveVideo() {
File root = new File(PATH_ROOT);
Task<Void> task = new Task<Void>() {
#Override
public Void call() throws Exception {
root.mkdirs();
for (int i = 0; i < recorded_frames.size(); i++) {
saveToFile(recorded_frames.get(i));
}
saveRecording(temp_frames);
logState("Amount of compiled frames: " + temp_frames.size());
return null;
}
};
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
}
/**
* A called to this method will add the frames store is array list
* to the video list.
* #param list: list containing the byte arrays of the frames
*/
private void loadFrames(ArrayList<byte[]> list) {
Task<Void> task = new Task<Void>() {
#Override
public Void call() throws Exception {
logState("loading frames...");
for (int i = 0; i < list.size(); i++) {
video_frames.add(byteToImage(list.get(i)));
}
logState("frames have been added!");
scaleVideo(video_size_scale);
return null;
}
};
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
}
/**
* Method which when called will add and display a indicator.
* #param rootPane: list to which the indicator will be added
* #param indicator: indicator to be displayed
*/
private void showIndicator(ObservableList<Node> rootPane, Indicator indicator) {
rootPane.removeAll(playing, idle, recording);
indicator.setTranslateX(width - ScaleX(150));
indicator.setTranslateY(ScaleY(100));
rootPane.add(indicator);
}
/**
* Calls to this method will save each frame if conditions are met
* and will also store each frame into a list of byte arrays.
* #param image: image to be saved to file and or converted and store as
* a byte array.
*/
private void saveToFile(Image image) {
counter += 1;
BufferedImage BImage = SwingFXUtils.fromFXImage(image, null);
temp_frames.add(ImageToByte(BImage));
if (saveFrames) {
File video = new File(PATH_FRAME + counter + "." + FILE_EXTENSION);
try {
ImageIO.write(BImage, FILE_EXTENSION, video);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
/**
* Method which when called loads images from a predefined
* directory in order to play them as a video.
*/
private void loadFromFile() {
Task<Void> task = new Task<Void>() {
#Override
public Void call() throws Exception {
for (int i = 1; i < 513; i++) {
File video = new File(PATH_FRAME + i + "." + FILE_EXTENSION);
video_frames.add(new ImageView(new Image(video.toURI().toString())));
}
return null;
}
};
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
}
/**
* Method which when called will attemp to save the video
* to a specified directory.
* #param list
*/
private void saveRecording(ArrayList<byte[]> list) {
Task<Void> task = new Task<Void>() {
#Override
public Void call() throws Exception {
File root = new File(PATH_ROOT);
File video = new File(PATH_VIDEO);
video.delete();
logState("Saving video...");
try {
root.mkdirs();
FileOutputStream fileOut = new FileOutputStream(PATH_VIDEO);
BufferedOutputStream bufferedStream = new BufferedOutputStream(fileOut);
ObjectOutputStream outputStream = new ObjectOutputStream(bufferedStream);
outputStream.writeObject(list);
outputStream.close();
fileOut.close();
logState("Video saved.");
} catch (IOException e) {
logState("Failed to save, I/O exception");
e.printStackTrace();
}
return null;
}
};
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
}
/**
* Method which when called attempts to retrieve the video
* from a specified directory
*/
#SuppressWarnings("unchecked")
private void retrieveRecording() {
Task<Void> task = new Task<Void>() {
#Override
public Void call() throws Exception {
File root = new File(PATH_ROOT);
File video = new File(PATH_VIDEO);
if (root.exists() && video.exists()) {
try {
FileInputStream fileIn = new FileInputStream(PATH_VIDEO);
ObjectInputStream inputStream = new ObjectInputStream(fileIn);
temp_frames = (ArrayList<byte[]>) inputStream.readObject();
inputStream.close();
fileIn.close();
logState("\nLoading video");
loadFrames(temp_frames);
} catch (IOException | ClassNotFoundException e) {
logState("Failed to load! " + e.getLocalizedMessage());
}
} else {
logState("Nothing to load.");
}
return null;
}
};
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
}
/**
* Method which when call creates a frame or snapshot of the
* given scene to be recorded.
* #return: frame taken from the scene.
*/
private synchronized Image create_frame() {
WritableImage wi = new WritableImage(width, height);
if (scene != null)
scene.snapshot(wi);
try {
return wi;
} finally {
wi = null;
}
}
/**
* Method which when called crates a frame or snapshot of the
* given node.
* #param node: node to be recorded
* #return: image or frame of recorded node.
*/
#SuppressWarnings("unused")
private synchronized Image create_node_frame(Node node) {
parameters.setFill(Color.TRANSPARENT);
WritableImage wi = new WritableImage((int)node.getBoundsInLocal().getWidth(), (int) node.getBoundsInLocal().getHeight());
node.snapshot(parameters, wi);
return wi;
}
/**
* Method which when called will create a scale relative to a base
* and current resolution.
* #param scaleX: x scaling factor used for manual scaling
* #param scaleY: y scaling factor used for manual scaling
* #param manualScaling: determines if a manual scaling will be applied or not
*/
public void scaleResolution(double scaleX, double scaleY, boolean manualScaling) {
double resolutionX = Screen.getPrimary().getBounds().getWidth();
double resolutionY = Screen.getPrimary().getBounds().getHeight();
double baseResolutionX = 1920;
double baseResolutionY = 1080;
bounds_scale_X = baseResolutionX / resolutionX;
bounds_scale_Y = baseResolutionY / resolutionY;
if(manualScaling==true){
bounds_scale_X = bounds_scale_X*scaleX;
bounds_scale_Y = bounds_scale_Y*scaleY;
}
}
public void allowRecording(boolean state) {
allowRecording = state;
logState("allowed recording: " + state);
}
public void allowPlayback(boolean state) {
allowPlayback = state;
logState("allowed playback: " + state);
}
public void setLocation(double x, double y) {
video_screen.setTranslateX(x);
video_screen.setTranslateY(y);
}
public void setDimensions(double width, double height) {
video_screen.setPrefSize(width, height);;
}
public void resetPlayback() {
this.frame = 0;
}
public double Scale(double value) {
double newSize = value * (bounds_scale_X + bounds_scale_Y)/2;
return newSize;
}
public double ScaleX(double value) {
double newSize = value * bounds_scale_X;
return newSize;
}
public double ScaleY(double value) {
double newSize = value * bounds_scale_Y;
return newSize;
}
public double getVideoWidth(){
if(!video_frames.isEmpty())
return video_frames.get(0).getImage().getWidth() * video_size_scale;
else{
return 0;
}
}
public double getVideoHeight(){
if(!video_frames.isEmpty())
return video_frames.get(0).getImage().getWidth() * video_size_scale;
else{
return 0;
}
}
#SuppressWarnings("unused")
private String loadResource(String image) {
String url = PATH_ROOT + image;
return url;
}
/**
* Method which converts a bufferedimage to byte array
* #param image: image to be converted
* #return: byte array of the image
*/
public final byte[] ImageToByte(BufferedImage image) {
byte[] imageInByte = null;
try {
if (image != null) {
ImageIO.write(image, FILE_EXTENSION, byteOutput);
imageInByte = byteOutput.toByteArray();
byteOutput.flush();
}
} catch (IOException | IllegalArgumentException e) {
e.printStackTrace();
}
try {
return imageInByte;
} finally {
byteOutput.reset();
}
}
/**
* Method which converts a byte array to a Imageview
* #param data: byte array to be converted.
* #return: imageview of the byte array
*/
public final ImageView byteToImage(byte[] data) {
BufferedImage newImage = null;
ImageView imageView = null;
Image image = null;
try {
InputStream inputStream = new ByteArrayInputStream(data);
newImage = ImageIO.read(inputStream);
inputStream.close();
image = SwingFXUtils.toFXImage(newImage, null);
imageView = new ImageView(image);
} catch (IOException e) {
e.printStackTrace();
}
return imageView;
}
private void logState(String state) {
System.out.println("JAVA_FX SCREEN RECORDER: " + state);
}
public enum PlaybackSettings {
CONTINUOUS_REPLAY, PLAY_ONCE,
}
/**
* Class which crates a simple indicator which can be
* used to display a recording or playing state
* #author Eudy Contreras
*
*/
private class Indicator extends HBox {
public Indicator(Color color, String message) {
Circle indicator = new Circle(Scale(15), color);
Text label = new Text(message);
label.setFont(Font.font("", FontWeight.EXTRA_BOLD, Scale(20)));
label.setFill(Color.WHITE);
setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
getChildren().addAll(indicator, label);
}
}
}
If you have any suggestions of things that will make this program more effecient please share with me. Thanks
I'm trying to implement a scene with a ScrollPane in which the user can drag a node around and scale it dynamically. I have the dragging and scaling with the mouse wheel working as well as a reset zoom, but I'm having trouble with the calculations to fit the node to the width of the parent.
Here is my code as an sscce.
(works) Mouse wheel will zoom in and out around the mouse pointer
(works) Left or right mouse press to drag the rectangle around
(works) Left double-click to reset the zoom
(doesn't work) Right double-click to fit the width
If I zoom in or out or change the window size, the fit to width does not work.
If anyone can help me with the calculations to fit the node to the width of the parent, I would very much appreciate it.
EDITED:
I marked the method that is not working correctly. It is fitWidth(), which is invoked by right mouse button double-clicking.
I edited the text of the question for clarity and focus
Hopefully this is more clear now.
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ZoomAndPanExample extends Application {
private ScrollPane scrollPane = new ScrollPane();
private final DoubleProperty zoomProperty = new SimpleDoubleProperty(1.0d);
private final DoubleProperty deltaY = new SimpleDoubleProperty(0.0d);
private final Group group = new Group();
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage primaryStage) {
scrollPane.setPannable(true);
scrollPane.setHbarPolicy(ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollBarPolicy.NEVER);
AnchorPane.setTopAnchor(scrollPane, 10.0d);
AnchorPane.setRightAnchor(scrollPane, 10.0d);
AnchorPane.setBottomAnchor(scrollPane, 10.0d);
AnchorPane.setLeftAnchor(scrollPane, 10.0d);
AnchorPane root = new AnchorPane();
Rectangle rect = new Rectangle(80, 60);
rect.setStroke(Color.NAVY);
rect.setFill(Color.NAVY);
rect.setStrokeType(StrokeType.INSIDE);
group.getChildren().add(rect);
// create canvas
PanAndZoomPane panAndZoomPane = new PanAndZoomPane();
zoomProperty.bind(panAndZoomPane.myScale);
deltaY.bind(panAndZoomPane.deltaY);
panAndZoomPane.getChildren().add(group);
SceneGestures sceneGestures = new SceneGestures(panAndZoomPane);
scrollPane.setContent(panAndZoomPane);
panAndZoomPane.toBack();
scrollPane.addEventFilter( MouseEvent.MOUSE_CLICKED, sceneGestures.getOnMouseClickedEventHandler());
scrollPane.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scrollPane.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scrollPane.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
root.getChildren().add(scrollPane);
Scene scene = new Scene(root, 600, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
class PanAndZoomPane extends Pane {
public static final double DEFAULT_DELTA = 1.3d;
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public DoubleProperty deltaY = new SimpleDoubleProperty(0.0);
private Timeline timeline;
public PanAndZoomPane() {
this.timeline = new Timeline(60);
// add scale transform
scaleXProperty().bind(myScale);
scaleYProperty().bind(myScale);
}
public double getScale() {
return myScale.get();
}
public void setScale( double scale) {
myScale.set(scale);
}
public void setPivot( double x, double y, double scale) {
// note: pivot value must be untransformed, i. e. without scaling
// timeline that scales and moves the node
timeline.getKeyFrames().clear();
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.millis(200), new KeyValue(translateXProperty(), getTranslateX() - x)),
new KeyFrame(Duration.millis(200), new KeyValue(translateYProperty(), getTranslateY() - y)),
new KeyFrame(Duration.millis(200), new KeyValue(myScale, scale))
);
timeline.play();
}
/**
* !!!! The problem is in this method !!!!
*
* The calculations are incorrect, and result in unpredictable behavior
*
*/
public void fitWidth () {
double scale = getParent().getLayoutBounds().getMaxX()/getLayoutBounds().getMaxX();
double oldScale = getScale();
double f = (scale / oldScale)-1;
double dx = getTranslateX() - getBoundsInParent().getMinX() - getBoundsInParent().getWidth()/2;
double dy = getTranslateY() - getBoundsInParent().getMinY() - getBoundsInParent().getHeight()/2;
double newX = f*dx + getBoundsInParent().getMinX();
double newY = f*dy + getBoundsInParent().getMinY();
setPivot(newX, newY, scale);
}
public void resetZoom () {
double scale = 1.0d;
double x = getTranslateX();
double y = getTranslateY();
setPivot(x, y, scale);
}
public double getDeltaY() {
return deltaY.get();
}
public void setDeltaY( double dY) {
deltaY.set(dY);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
public class SceneGestures {
private DragContext sceneDragContext = new DragContext();
PanAndZoomPane panAndZoomPane;
public SceneGestures( PanAndZoomPane canvas) {
this.panAndZoomPane = canvas;
}
public EventHandler<MouseEvent> getOnMouseClickedEventHandler() {
return onMouseClickedEventHandler;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
public EventHandler<ScrollEvent> getOnScrollEventHandler() {
return onScrollEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
sceneDragContext.mouseAnchorX = event.getX();
sceneDragContext.mouseAnchorY = event.getY();
sceneDragContext.translateAnchorX = panAndZoomPane.getTranslateX();
sceneDragContext.translateAnchorY = panAndZoomPane.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
panAndZoomPane.setTranslateX(sceneDragContext.translateAnchorX + event.getX() - sceneDragContext.mouseAnchorX);
panAndZoomPane.setTranslateY(sceneDragContext.translateAnchorY + event.getY() - sceneDragContext.mouseAnchorY);
event.consume();
}
};
/**
* Mouse wheel handler: zoom to pivot point
*/
private EventHandler<ScrollEvent> onScrollEventHandler = new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double delta = PanAndZoomPane.DEFAULT_DELTA;
double scale = panAndZoomPane.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
panAndZoomPane.setDeltaY(event.getDeltaY());
if (panAndZoomPane.deltaY.get() < 0) {
scale /= delta;
} else {
scale *= delta;
}
double f = (scale / oldScale)-1;
double dx = (event.getX() - (panAndZoomPane.getBoundsInParent().getWidth()/2 + panAndZoomPane.getBoundsInParent().getMinX()));
double dy = (event.getY() - (panAndZoomPane.getBoundsInParent().getHeight()/2 + panAndZoomPane.getBoundsInParent().getMinY()));
panAndZoomPane.setPivot(f*dx, f*dy, scale);
event.consume();
}
};
/**
* Mouse click handler
*/
private EventHandler<MouseEvent> onMouseClickedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.getButton().equals(MouseButton.PRIMARY)) {
if (event.getClickCount() == 2) {
panAndZoomPane.resetZoom();
}
}
if (event.getButton().equals(MouseButton.SECONDARY)) {
if (event.getClickCount() == 2) {
panAndZoomPane.fitWidth();
}
}
}
};
}
}
I found the answer. I was looking at the wrong calculations, assuming it to be related to the translations. The real culprit was the calculation for the difference in scale. I simply changed this:
double f = (scale / oldScale)-1;
to this:
double f = scale - oldScale;
I had written a code to move a ball with the help of accelerometer andengine, but i get force close.
My problem is the use of PhysicsWorld.
My library is:
andengine.jar
andenginephysicsbox2dextension.jar
my code is:
public class SnakeGameActivity extends BaseGameActivity implements IAccelerometerListener
{
private static final int CAMERA_WIDTH = 480;
private static final int CAMERA_HEIGHT = 320;
private float mGravityX;
private float mGravityY;
private Sprite splash ;
private final Vector2 mTempVector = new Vector2();
protected PhysicsWorld mPhysicsWorld;
private Camera mCamera;
private Texture mTexture;
private TextureRegion mSplashTextureRegion;
#Override
public Engine onLoadEngine() {
// TODO Auto-generated method stub
this.mCamera = new Camera(0, 0, CAMERA_WIDTH, CAMERA_HEIGHT);
return new Engine(new EngineOptions(true, ScreenOrientation.LANDSCAPE,
new RatioResolutionPolicy(CAMERA_WIDTH, CAMERA_HEIGHT), this.mCamera));
}
#Override
public void onLoadResources() {
// TODO Auto-generated method stub
this.mTexture = new Texture(512, 512, TextureOptions.BILINEAR_PREMULTIPLYALPHA);
this.enableAccelerometerSensor(this);
this.mSplashTextureRegion = TextureRegionFactory.createFromAsset(this.mTexture, this, "ball_enemy.png", 0, 0);
this.mEngine.registerUpdateHandler(new FPSLogger());
this.mEngine.getTextureManager().loadTexture(this.mTexture);
}
#Override
public Scene onLoadScene() {
this.mEngine.registerUpdateHandler(new FPSLogger());
Scene scene = new Scene(1);
this.mPhysicsWorld = new PhysicsWorld(new Vector2(0, SensorManager.GRAVITY_EARTH), false);
final int centerX = (CAMERA_WIDTH - this.mSplashTextureRegion.getWidth()) / 2;
final int centerY = (CAMERA_HEIGHT - this.mSplashTextureRegion.getHeight()) / 2;
splash = new Sprite(centerX, centerY, this.mSplashTextureRegion);
scene.getLastChild().attachChild(splash);
scene.registerUpdateHandler(this.mPhysicsWorld);
return scene;
}
#Override
public void onLoadComplete() {
// TODO Auto-generated method stub
}
public void onAccelerometerChanged(AccelerometerData pAccelerometerData) {
final Vector2 gravity = Vector2Pool.obtain(pAccelerometerData.getX(), pAccelerometerData.getY());
this.mPhysicsWorld.setGravity(gravity);
Vector2Pool.recycle(gravity);
}
}
my log error:
02-25 21:08:22.334: E/ActivityManager(69): Start proc com.android.deskclock for
broadcast com.android.deskclock/.AlarmInitReceiver: pid=255 uid=10019 gids={}
CPU usage from 16953ms to 19171ms later:
Thank you very much for your help