I have to animations in my Kotlin Scene and I want to make a sequence out of these two and I want the sequence to have infinite repetitions like looping the sequence.
//First Animation
ObjectAnimator.ofFloat(block, "translationX", 50f).apply {
duration = 500
start()
}
//Second Animation
ObjectAnimator.ofFloat(block, "translationX", 0f).apply {
duration = 500
start()
}
Thanks for your help!
Solution:
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import kotlinx.android.synthetic.main.activity_main.*
import android.animation.ValueAnimator
import android.animation.ObjectAnimator
import android.animation.AnimatorSet
import android.animation.Animator
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
animateTogether(
stripe.objectAnimate() // https://github.com/blipinsk/ViewPropertyObjectAnimator
.translationX(100f)
.get(),
stripe.objectAnimate()
.translationX(-100f)
.get()
).start()
}
fun animateTogether(vararg animators: Animator): AnimatorSet =
AnimatorSet().apply {
ObjectAnimator.ofFloat(stripe, "translationX", 100f).apply {
duration = 500
start()
}
ObjectAnimator.ofFloat(stripe, "translationX", -100f).apply {
duration = 500
start()
}
}
Try AnimatorSet
AnimatorSet as = new AnimatorSet();
as.playSequentially(ObjectAnimator.ofFloat(...),
ObjectAnimator.ofFloat(...));
then you have two ways to loop it one set repeat mode on it's each animators taken from here
objectAnimator.setRepeatCount(ObjectAnimator.INFINITE);
objectAnimator.setRepeatMode(ObjectAnimator.RESTART/REVERSE...);
second use AnimatorListenerAdapter to listen when animation ends and restart the same animation.
mAnimationSet.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mAnimationSet.start();
}
});
mAnimationSet.start();
I like to do the following:
fun View.objectAnimate() = ViewPropertyObjectAnimator.animate(this) // https://github.com/blipinsk/ViewPropertyObjectAnimator
fun animateTogether(vararg animators: Animator): AnimatorSet =
AnimatorSet().apply {
playTogether(*animators)
}
And now I can do
animateTogether(
someView.objectAnimate()
.translationX(40.dp)
.get(),
otherView.objectAnimate()
.translationX(-40.dp)
.get()
).start()
For sequence, you can play with .duration and .initialDelay values of Animator, and can set repeat mode, etc.
Related
I am doing a simple time slot (planning entity) -> team (planning var) assignment task. All work fine while using only one thread. Also multithreading works fine if one custom move I have is not involved, but after it's used the solver gets stuck forever with no message. All the custom moves are created before every STEP. Full assert doesn't say anything. It looks like deadlock for me. I am using optaplanner 8.25.0.Final with Java 17
This is the move factory:
package pl.medaxtrans.pre.solver;
import org.optaplanner.core.impl.heuristic.move.Move;
import org.optaplanner.core.impl.heuristic.selector.move.factory.MoveListFactory;
import pl.medaxtrans.common.domain.Team;
import pl.medaxtrans.pre.domain.HalfHourTimeslot;
import pl.medaxtrans.pre.domain.ShiftsSolution;
import java.time.DayOfWeek;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class FarSlotChangeMoveFactory implements MoveListFactory<ShiftsSolution> {
private static Predicate<HalfHourTimeslot> equalTime(HalfHourTimeslot slot2) {
return slot -> slot.compareTo(slot2) == 0;
}
private static Predicate<HalfHourTimeslot> sameDayAndTeam(DayOfWeek day, Team team) {
return slot -> slot.getDayOfWeek().equals(day) && slot.getTeam().equals(team);
}
#Override
public List<? extends Move<ShiftsSolution>> createMoveList(ShiftsSolution solution) {
List<FarSlotChangeMove> moves = new ArrayList<>();
List<HalfHourTimeslot> slots = solution.getSlots();
List<Team> teams = solution.getTeams();
for (Team fromTeam : teams) {
for (Team toTeam : teams) {
if (fromTeam != toTeam) {
for (DayOfWeek day : DayOfWeek.values()) {
List<HalfHourTimeslot> fromTeamSlots = slots.stream().filter(sameDayAndTeam(day, fromTeam)).collect(Collectors.toList());
List<HalfHourTimeslot> toTeamSlots = slots.stream().filter(sameDayAndTeam(day, toTeam)).collect(Collectors.toList());
Optional<HalfHourTimeslot> fromMinOpt = fromTeamSlots.stream().min(Comparator.naturalOrder());
Optional<HalfHourTimeslot> fromMaxOpt = fromTeamSlots.stream().max(Comparator.naturalOrder());
Optional<HalfHourTimeslot> toMaxOpt = toTeamSlots.stream().max(Comparator.naturalOrder());
Optional<HalfHourTimeslot> toMinOpt = toTeamSlots.stream().min(Comparator.naturalOrder());
if (fromMinOpt.isPresent() && fromMaxOpt.isPresent() && toMaxOpt.isPresent() && toMinOpt.isPresent())
{
HalfHourTimeslot fromMin = fromMinOpt.get();
HalfHourTimeslot fromMax = fromMaxOpt.get();
HalfHourTimeslot toMax = toMaxOpt.get();
HalfHourTimeslot toMin = toMinOpt.get();
if (toMin.compareTo(fromMin) < 0 && toMax.compareTo(fromMin) > 0 && toTeamSlots.stream().noneMatch(equalTime(fromMin)))
{
moves.add(new FarSlotChangeMove(fromMin, toTeam));
}
if (toMin.compareTo(fromMax) < 0 && toMax.compareTo(fromMax) > 0 && toTeamSlots.stream().noneMatch(equalTime(fromMax)))
{
moves.add(new FarSlotChangeMove(fromMax, toTeam));
}
}
}
}
}
}
return moves;
}
}
And this is the problematic move:
package pl.medaxtrans.pre.solver;
import org.optaplanner.core.api.score.director.ScoreDirector;
import org.optaplanner.core.impl.heuristic.move.AbstractMove;
import pl.medaxtrans.common.domain.Team;
import pl.medaxtrans.pre.domain.HalfHourTimeslot;
import pl.medaxtrans.pre.domain.ShiftsSolution;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
public class FarSlotChangeMove extends AbstractMove<ShiftsSolution> {
private final HalfHourTimeslot slot;
private final Team toTeam;
private final Team fromTeam;
public FarSlotChangeMove(HalfHourTimeslot slot, Team toTeam) {
this.slot = slot;
this.toTeam = toTeam;
this.fromTeam = slot.getTeam();
}
#Override
protected AbstractMove<ShiftsSolution> createUndoMove(ScoreDirector<ShiftsSolution> scoreDirector) {
return new FarSlotChangeMove(slot, fromTeam);
}
#Override
protected void doMoveOnGenuineVariables(ScoreDirector<ShiftsSolution> scoreDirector) {
scoreDirector.beforeVariableChanged(slot, "team");
slot.setTeam(toTeam);
scoreDirector.afterVariableChanged(slot, "team");
}
#Override
public boolean isMoveDoable(ScoreDirector<ShiftsSolution> scoreDirector) {
// we don't check overlaps here, because it is already checked by the move factory
return !toTeam.equals(fromTeam);
}
#Override
public FarSlotChangeMove rebase(ScoreDirector<ShiftsSolution> destinationScoreDirector) {
return new FarSlotChangeMove(destinationScoreDirector.lookUpWorkingObject(slot), destinationScoreDirector.lookUpWorkingObject(toTeam));
}
#Override
public Collection<?> getPlanningEntities() {
return Collections.singletonList(slot);
}
#Override
public Collection<?> getPlanningValues() {
return Collections.singletonList(toTeam);
}
#Override
public String toString() {
return slot + " {" + fromTeam + "->" + toTeam + "}";
}
#Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final FarSlotChangeMove other = (FarSlotChangeMove) o;
return Objects.equals(fromTeam, other.fromTeam) &&
Objects.equals(toTeam, other.toTeam) &&
Objects.equals(slot, other.slot);
}
#Override
public int hashCode() {
return Objects.hash(slot, toTeam, fromTeam);
}
}
After using this move and right after this trace message (after cachedMoveList is empty):
2022-09-14 20:13:28,568 [main] TRACE Created cachedMoveList: size (0), moveSelector (MoveListFactory(class pl.medaxtrans.pre.solver.FarSlotChangeMoveFactory)).
All solver threads get blocked in "WAIT" mode and they never stop waiting. What are they waiting for and what did I do wrong? This is expected that this move list ends at some point and I want solver to stop solving then. Why is it not happening as opposed to single thread solving? On the other hand while solving with one thread only I get:
No doable selected move at step index ... Terminating phase early.
And it ends. I want the exact same behavior with multithreading.
When I run my app, it is supposed to show a screen for a user to create an account or sign in. I had the code working as a regular kotlin app and decided to make a multiplatform app instead so I remade the project as multiplatform. KMM does not support this code:
import kotlinx.android.synthetic.main.activity_sign_in.*
so I had to change to ViewBinding. Here is the code I used previously and what I changed it to:
Before:
import android.content.Intent
import android.os.Bundle
import android.widget.EditText
import com.cj.globekotlin.Extensions.toast
import com.cj.globekotlin.FirebaseUtils.firebaseAuth
import kotlinx.android.synthetic.main.activity_sign_in.*
class SignInActivity : AppCompatActivity() {
lateinit var signInEmail: String
lateinit var signInPassword: String
lateinit var signInInputsArray: Array<EditText>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sign_in)
signInInputsArray = arrayOf(etSignInEmail, etSignInPassword)
btnCreateAccount2.setOnClickListener {
startActivity(Intent(this, CreateAccountActivity::class.java))
finish()
}
btnSignIn.setOnClickListener {
signInUser()
}
}
private fun notEmpty(): Boolean = signInEmail.isNotEmpty() && signInPassword.isNotEmpty()
private fun signInUser() {
signInEmail = etSignInEmail.text.toString().trim()
signInPassword = etSignInPassword.text.toString().trim()
if (notEmpty()) {
firebaseAuth.signInWithEmailAndPassword(signInEmail, signInPassword)
.addOnCompleteListener { signIn ->
if (signIn.isSuccessful) {
startActivity(Intent(this, HomeActivity::class.java))
toast("signed in successfully")
finish()
} else {
toast("sign in failed")
}
}
} else {
signInInputsArray.forEach { input ->
if (input.text.toString().trim().isEmpty()) {
input.error = "${input.hint} is required"
}
}
}
}
}
Current:
import android.content.Intent
import android.os.Bundle
import android.widget.EditText
import com.cj.globemultiplatform.android.Extensions.toast
import com.cj.globemultiplatform.android.FirebaseUtils.firebaseAuth
import com.cj.globemultiplatform.android.databinding.ActivitySignInBinding
class SignInActivity : AppCompatActivity() {
lateinit var signInEmail: String
lateinit var signInPassword: String
lateinit var signInInputsArray: Array<EditText>
private lateinit var binding: ActivitySignInBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySignInBinding.inflate(layoutInflater)
setContentView(R.layout.activity_sign_in)
signInInputsArray = arrayOf(binding.etSignInEmail, binding.etSignInPassword)
binding.btnCreateAccount2.setOnClickListener {
startActivity(Intent(this, CreateAccountActivity::class.java))
finish()
}
binding.btnSignIn.setOnClickListener {
signInUser()
}
}
private fun notEmpty(): Boolean = signInEmail.isNotEmpty() && signInPassword.isNotEmpty()
private fun signInUser() {
signInEmail = binding.etSignInEmail.text.toString().trim()
signInPassword = binding.etSignInPassword.text.toString().trim()
if (notEmpty()) {
firebaseAuth.signInWithEmailAndPassword(signInEmail, signInPassword)
.addOnCompleteListener { signIn ->
if (signIn.isSuccessful) {
startActivity(Intent(this, HomeActivity::class.java))
toast("signed in successfully")
finish()
} else {
toast("sign in failed")
}
}
} else {
signInInputsArray.forEach { input ->
if (input.text.toString().trim().isEmpty()) {
input.error = "${input.hint} is required"
}
}
}
}
}
Initially, I thought that this would work but now whenever I open the app, it crashes. I think this change is what's causing the crashes. Is that possible? If so, how can I fix this?
There are a couple of problems with your android code:
There is no connection between your inflated binding and the content you are setting, you should use
binding = ActivitySignInBinding.inflate(layoutInflater)
setContentView(binding.root)
Your lateinit variables for the inputs are used in the signInUser() method, but it's not initialised. I'd suggest removing that and using binding.signInEmail and the other views.
Also, if you're trying to share code between Android & iOS, you should be aware that you'll need to abstract away any platform specific implementation. For ex: all packages that are android specific will not work on iOS at this moment. This specific code is pretty Android platform heavy, thus I wouldn't even try to share this in a KMM app, only business logic, perhaps up to a ViewModel layer.
I am solving a tough problem in OptaPlanner. The best algorithm I found so far is to use a custom move factory, a computationally intensive one. After noticing that I was utilising a single CPU core, I discovered that OptaPlanner only spreads on multiple threads the score calculation, while it performs the move generation in a single thread.
To mitigate the problem, I implemented the multi-threading in my move factory via the following abstract class, which I then extend with the actual logic (I did this because I actually have three computationally expensive custom move factories):
package my.solver.move;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import org.optaplanner.core.impl.domain.solution.descriptor.SolutionDescriptor;
import org.optaplanner.core.impl.heuristic.move.CompositeMove;
import org.optaplanner.core.impl.heuristic.move.Move;
import org.optaplanner.core.impl.heuristic.selector.move.factory.MoveIteratorFactory;
import org.optaplanner.core.impl.score.director.ScoreDirector;
import java.util.Iterator;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
public abstract class MultiThreadedMoveFactory<T> implements MoveIteratorFactory<T> {
private final ThreadPoolExecutor threadPoolExecutor;
public MultiThreadedMoveFactory(
#NonNull String threadPrefix
) {
int availableProcessorCount = Runtime.getRuntime().availableProcessors();
int resolvedThreadCount = Math.max(1, availableProcessorCount);
ThreadFactory threadFactory = new SolverThreadFactory(threadPrefix);
threadPoolExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(resolvedThreadCount, threadFactory);
}
#AllArgsConstructor
public class MoveGeneratorData {
T solution;
SolutionDescriptor<T> solutionDescriptor;
Random random;
BlockingQueue<Move<T>> generatedMoves;
}
protected abstract int getNumMoves();
#Override
public long getSize(ScoreDirector<T> scoreDirector) {
return getNumMoves();
}
protected class MovesIterator implements Iterator<Move<T>> {
private final BlockingQueue<Move<T>> generatedMoves = new ArrayBlockingQueue<>(getNumMoves());
public MovesIterator(
#NonNull T solution,
#NonNull SolutionDescriptor<T> solutionDescriptor,
#NonNull Random random,
#NonNull Function<MoveGeneratorData, Runnable> moveGeneratorFactory
) {
MoveGeneratorData moveGeneratorData = new MoveGeneratorData(solution, solutionDescriptor, random, generatedMoves);
for (int i = 0; i < getNumMoves(); i++) {
threadPoolExecutor.submit(moveGeneratorFactory.apply(moveGeneratorData));
}
}
#Override
public boolean hasNext() {
if (!generatedMoves.isEmpty()) {
return true;
}
while (threadPoolExecutor.getActiveCount() > 0) {
try {
//noinspection BusyWait
Thread.sleep(50);
} catch (InterruptedException e) {
return false;
}
}
return !generatedMoves.isEmpty();
}
#Override
public Move<T> next() {
//noinspection unchecked
return Objects.requireNonNullElseGet(generatedMoves.poll(), CompositeMove::new);
}
}
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private static class SolverThreadFactory implements ThreadFactory {
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
public SolverThreadFactory(String threadPrefix) {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
namePrefix = "MyPool-" + poolNumber.getAndIncrement() + "-" + threadPrefix + "-";
}
#Override
public Thread newThread(#NonNull Runnable r) {
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
if (t.isDaemon()) {
t.setDaemon(false);
}
if (t.getPriority() != Thread.NORM_PRIORITY) {
t.setPriority(Thread.NORM_PRIORITY);
}
return t;
}
}
#Override
public Iterator<? extends Move<T>> createOriginalMoveIterator(ScoreDirector<T> scoreDirector) {
return createMoveIterator(scoreDirector, new Random());
}
#Override
public Iterator<? extends Move<T>> createRandomMoveIterator(ScoreDirector<T> scoreDirector, Random workingRandom) {
return createMoveIterator(scoreDirector, workingRandom);
}
public abstract Iterator<? extends Move<T>> createMoveIterator(ScoreDirector<T> scoreDirector, Random random);
}
However, the solver seems to hang after a while. The debugger tells me that it's waiting on an innerQueue.take() in OrderByMoveIndexBlockingQueue. This is caused by my move factory: if I revert the above and only use the previous implementation, which was single-threaded, the problem goes away.
I do not quite understand where the problem is, so the question is: how can I fix it?
No, no, no. This approach is doomed. I think. (Prove me wrong.)
JIT selection
First learn about Just In Time selection (see docs) of moves.
Instead of generating all moves (which can be billions) at the beginning of each step, only generate those that will actually be evaluated. Most LS algorithms will only evaluate a few moves per step.
Watch the TRACE log to see how many milliseconds it takes to start a step. Typically you want to do evaluate 10000 moves per second, so it should take 0 or 1 milliseconds to start a step (the log only shows in milliseconds).
Multithreaded solving
Then learn about moveThreadCount to enable multithreaded solving. See this blog post. Know that this still does the move selection on 1 thread, for reproducibility reasons. But the move evaluation is spread across threads.
Caching for move selection
But your custom moves are smart, so the move selection must be smart?
First determine what "solution state" query information you need to generate the moves - for example a Map<Employee, List<Shift>> - then cache that:
either calculate that map at the beginning of each step, if it doesn't take too long (but this won't scale because it doesn't do deltas)
or use a shadow variable (#InverseRelationShadowVariable works fine in this case), because these are updated through deltas. But it does do the delta's for every move and undo move too...
Or hack in an actual new MoveSelector, which can listen to stepEnded() events and actually apply the delta of the last step on that Map, without doing any of the deltas of every move and undo move. We should probably standardize this approach and make it part of our public API some day.
I was able to make the factory work by removing any trace of JIT-ing from hasNext: block the method until all moves have been generated, and only then return true, and keep returning true until all moves have been consumed.
#Override
public boolean hasNext() {
while (!generationComplete && generatedMoves.size() < getNumMoves()) {
try {
// We get a warning because the event we are waiting for could happen earlier than the end of sleep
// and that means we would be wasting time, but that is negligible so we silence it
//noinspection BusyWait
Thread.sleep(50);
} catch (InterruptedException e) {
return false;
}
}
generationComplete = true;
return !generatedMoves.isEmpty();
}
To the best of my understanding, the solution I am using not only works, but it is the best I found in a few months of iterations.
I need to create an action in my custom business process that must be executed every 10 minutes until an specific action is returned, is there any way to customize the polling interval of an action in hybris order business process? I know that you can configure a timeout but not a polling interval:
<wait id='waitForOrderConfirmation' then='checkOrder' prependProcessCode='true'>
<event>confirm</event>
<timeout delay='PT12H' then='asmCancelOrder'/>
Need custom Implementation to achieve this and need to use BusinessProcessParameterModel.
Below are the steps to Make Retry based on Dealy.
Create RepeatableAction.
import de.hybris.platform.processengine.model.BusinessProcessModel;
import de.hybris.platform.processengine.model.BusinessProcessParameterModel;
import de.hybris.platform.servicelayer.model.ModelService;
import de.hybris.platform.warehousing.process.BusinessProcessException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Optional;
public interface RepeatableAction<T extends BusinessProcessModel>
{
int COUNTER_STARTER = 1;
ModelService getModelService();
Optional<Integer> extractThreshold(T process);
String getParamName();
default void increaseRetriesCounter(final T process)
{
final Collection<BusinessProcessParameterModel> contextParameters = process.getContextParameters();
final Optional<BusinessProcessParameterModel> paramOptional = extractCounter(contextParameters);
paramOptional.ifPresent(this::incrementParameter);
if (!paramOptional.isPresent())
{
final Collection<BusinessProcessParameterModel> newContextParameters = new ArrayList<>(contextParameters);
final BusinessProcessParameterModel counter = getModelService().create(BusinessProcessParameterModel.class);
counter.setName(getParamName());
counter.setValue(COUNTER_STARTER);
counter.setProcess(process);
newContextParameters.add(counter);
process.setContextParameters(newContextParameters);
getModelService().save(process);
}
}
default Optional<BusinessProcessParameterModel> extractCounter(
final Collection<BusinessProcessParameterModel> contextParameters)
{
//#formatter:off
return contextParameters.stream().filter(p -> getParamName().equals(p.getName())).findFirst();
//#formatter:on
}
default void incrementParameter(final BusinessProcessParameterModel parameter)
{
final Object value = parameter.getValue();
if (value instanceof Integer)
{
parameter.setValue((Integer) value + 1);
getModelService().save(parameter);
}
else
{
//#formatter:off
final String message = MessageFormat.format("Wrong process parameter '{}' type. {} expected, {} actual.", getParamName(),
Integer.class.getSimpleName(), value.getClass().getSimpleName());
//#formatter:on
throw new BusinessProcessException(message);
}
}
default boolean retriesCountThresholdExceeded(final T process)
{
//#formatter:off
final Optional<Integer> counterOptional = extractCounter(process.getContextParameters())
.map(BusinessProcessParameterModel::getValue).filter(Integer.class::isInstance).map(Integer.class::cast);
//#formatter:on
final Optional<Integer> thresholdOptional = extractThreshold(process);
final boolean counterSet = counterOptional.isPresent();
final boolean thresholdSet = thresholdOptional.isPresent();
boolean thresholdExceeded = false;
if (counterSet && thresholdSet)
{
final int counter = counterOptional.get();
final int threshold = thresholdOptional.get();
thresholdExceeded = counter > threshold;
}
return counterSet && thresholdSet && thresholdExceeded;
}
}
Then Go to Custom Action and Implement This Interface and based on some condition make custom Transition as RETRY, something like this.
public class CustomAction extends AbstractAction<OrderProcessModel>
implements RepeatableAction<OrderProcessModel>
{
private static final int MAX_RETRIES = 3;//make it configurable it's your choice
#Override
public Transition prepare(OrderModel order, OrderProcessModel process)
{
if (!retriesCountThresholdExceeded(process))
{
if (custom condition)
{
return Transition.OK;
}
getModelService().refresh(order);
increaseRetriesCounter(process);
return Transition.RETRY;
}
return Transition.NOK;
}
}
#Override
public Optional<Integer> extractThreshold(OrderProcessModel process)
{
return Optional.of(MAX_RETRIES);
}
Then in process.xml Action entries should be like this.
<action id="customAction" bean="customAction">
<transition name="OK" to="nextStep"/>
<transition name="RETRY" to="waitForOrderConfirmation"/>
<transition name="NOK" to="cancelOrderAction"/>
</action>
<wait id='waitForOrderConfirmation' then='checkOrder' prependProcessCode='true'>
<event>confirm</event>
<timeout delay='PT12H' then='asmCancelOrder'/>
<wait>
NOTE: Please set dealy as per requirement as of new 12hr seems to be too much
I thought this would be a simple question but I am having trouble finding an answer. I have a single ImageView object associated with a JavaFX Scene object and I want to load large images in from disk and display them in sequence one after another using the ImageView. I have been trying to find a good way to repeatedly check the Image object and when it is done loading in the background set it to the ImageView and then start loading a new Image object. The code I have come up with (below) works sometimes and sometimes it doesn't. I am pretty sure I am running into issues with JavaFX and threads. It loads the first image sometimes and stops. The variable "processing" is a boolean instance variable in the class.
What is the proper way to load an image in JavaFX in the background and set it to the ImageView after it is done loading?
public void start(Stage primaryStage) {
...
ImageView view = new ImageView();
((Group)scene.getRoot()).getChildren().add(view);
...
Thread th = new Thread(new Thread() {
public void run() {
while(true) {
if (!processing) {
processing = true;
String filename = files[count].toURI().toString();
Image image = new Image(filename,true);
image.progressProperty().addListener(new ChangeListener<Number>() {
#Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number progress) {
if ((Double) progress == 1.0) {
if (! image.isError()) {
view.setImage(image);
}
count++;
if (count == files.length) {
count = 0;
}
processing = false;
}
}
});
}
}
}
});
}
I actually think there's probably a better general approach to satisfying whatever your application's requirements are than the approach you are trying to use, but here is my best answer at implementing the approach you describe.
Create a bounded BlockingQueue to hold the images as you load them. The size of the queue may need some tuning: too small and you won't have any "buffer" (so you won't be able to take advantage of any that are faster to load than the average), too large and you might consume too much memory. The BlockingQueue allows you to access it safely from multiple threads.
Create a thread that simply loops and loads each image synchronously, i.e. that thread blocks while each image loads, and deposits them in the BlockingQueue.
Since you want to try to display images up to once per FX frame (i.e. 60fps), use an AnimationTimer. This has a handle method that is invoked on each frame render, on the FX Application Thread, so you can implement it just to poll() the BlockingQueue, and if an image was available, set it in the ImageView.
Here's an SSCCE. I also indicated how to do this where you display each image for a fixed amount of time, as I think that's a more common use case and might help others looking for similar functionality.
import java.io.File;
import java.net.MalformedURLException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.animation.AnimationTimer;
import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
public class ScreenSaver extends Application {
#Override
public void start(Stage primaryStage) {
BorderPane root = new BorderPane();
Button startButton = new Button("Choose image directory...");
startButton.setOnAction(e -> {
DirectoryChooser chooser= new DirectoryChooser();
File dir = chooser.showDialog(primaryStage);
if (dir != null) {
File[] files = Stream.of(dir.listFiles()).filter(file -> {
String fName = file.getAbsolutePath().toLowerCase();
return fName.endsWith(".jpeg") | fName.endsWith(".jpg") | fName.endsWith(".png");
}).collect(Collectors.toList()).toArray(new File[0]);
root.setCenter(createScreenSaver(files));
}
});
root.setCenter(new StackPane(startButton));
primaryStage.setScene(new Scene(root, 800, 800));
primaryStage.show();
}
private Parent createScreenSaver(File[] files) {
ImageView imageView = new ImageView();
Pane pane = new Pane(imageView);
imageView.fitWidthProperty().bind(pane.widthProperty());
imageView.fitHeightProperty().bind(pane.heightProperty());
imageView.setPreserveRatio(true);
Executor exec = Executors.newCachedThreadPool(runnable -> {
Thread t = new Thread(runnable);
t.setDaemon(true);
return t ;
});
final int imageBufferSize = 5 ;
BlockingQueue<Image> imageQueue = new ArrayBlockingQueue<Image>(imageBufferSize);
exec.execute(() -> {
int index = 0 ;
try {
while (true) {
Image image = new Image(files[index].toURI().toURL().toExternalForm(), false);
imageQueue.put(image);
index = (index + 1) % files.length ;
}
} catch (MalformedURLException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// This will show a new image every single rendering frame, if one is available:
AnimationTimer timer = new AnimationTimer() {
#Override
public void handle(long now) {
Image image = imageQueue.poll();
if (image != null) {
imageView.setImage(image);
}
}
};
timer.start();
// This wait for an image to become available, then show it for a fixed amount of time,
// before attempting to load the next one:
// Duration displayTime = Duration.seconds(1);
// PauseTransition pause = new PauseTransition(displayTime);
// pause.setOnFinished(e -> exec.execute(createImageDisplayTask(pause, imageQueue, imageView)));
// exec.execute(createImageDisplayTask(pause, imageQueue, imageView));
return pane ;
}
private Task<Image> createImageDisplayTask(PauseTransition pause, BlockingQueue<Image> imageQueue, ImageView imageView) {
Task<Image> imageDisplayTask = new Task<Image>() {
#Override
public Image call() throws InterruptedException {
return imageQueue.take();
}
};
imageDisplayTask.setOnSucceeded(e -> {
imageView.setImage(imageDisplayTask.getValue());
pause.playFromStart();
});
return imageDisplayTask ;
}
public static void main(String[] args) {
launch(args);
}
}