javaandroidandroid-animationondrawandroid-view-invalidate

OnDraw() only called once after loop


I have a program I am writing in android studio that is supposed to play a music note and have a circle expand in size based off an arraylist of coordinates, song-note names and times to play the notes. However, the problem I am having is that all of the notes play correctly but only the last circle gets drawn and it's after I exit the loop that plays all the notes. I looked up this question before, and I got some answers about calling setWillNotDraw(false);, but I called that in my constructor and I'm still having the same issue. I've tried calling invalidate(); in multiple different places, but nothing seems to be working. Any help would be greatly appreciated!

My code:

Constructor

public PlayView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    saver = PersistentSaver.getInstance(context);
    this.context=context;
    setWillNotDraw(false);
}

On draw

@Override
public void onDraw(Canvas canvas){
    super.onDraw(canvas);
    this.canvas=canvas;
    if(mCurrentRect !=null&& colorPaint!=null) {
        canvas.drawOval(mCurrentRect, colorPaint);
    }
}

The loop to play the notes- I have checked, all circle coordinates and notes are contained in the correct arrays:

public void playSong(){
    String song = saver.getCurrSongName();
    ArrayList<Long> Times = saver.getTimesForNotes(song);
    ArrayList<Float> xCoordinates = saver.getPressXCoordinates(song);
    ArrayList<Float> yCoordinates = saver.getPressYCoordinates(song);
    ArrayList<String> notes = saver.getNotesForSong(song);
    for(int i=0;i<Times.size();i++) {
        //pause until correct time. TODO: ADJUST ERROR BOUND
        while (true) {
            currTime = System.currentTimeMillis() - startTime;
            if (currTime <= Times.get(i) + epsilon && currTime >= Times.get(i) - epsilon) break;
        }
        //play note with that x and y coordinate and draw circle
        playNote(xCoordinates.get(i), yCoordinates.get(i), notes.get(i));
    }
}

The code to play a note and the sound:

    private void playNote(float pressx, float pressy, String note){
    colorPaint=new Paint();
    colorPaint.setStyle(Paint.Style.STROKE);
    colorPaint.setStrokeWidth(10);
    colorPaint.setColor(generateColor(pressx,pressy));
    float translateX = 50.0f;
    float translateY = 50.0f;
    Random rand = new Random(10000);
    int xr = rand.nextInt();
    int yr = rand.nextInt();
    int startColor = generateColor(pressx,pressy);
    int midColor = generateColor(pressx+rand.nextInt(),pressy+rand.nextInt());
    int endColor = generateColor(pressx+xr,pressy+yr);
    mCurrentRect = new AnimatableRectF(pressx-50,pressy-50,pressx+50,pressy+50);
    debugx=pressx;
    playAnimation(startColor,midColor,endColor, translateX, translateY);
    playSound(note);
}

The code that is supposed to play the animation but is only calling onDraw(); after the loop exits

    private void playAnimation(int startColor, int midColor, int endColor, float translateX, float translateY){
    ObjectAnimator animateLeft = ObjectAnimator.ofFloat(mCurrentRect, "left", mCurrentRect.left, mCurrentRect.left-translateX);
    ObjectAnimator animateRight = ObjectAnimator.ofFloat(mCurrentRect, "right", mCurrentRect.right, mCurrentRect.right+translateX);
    ObjectAnimator animateTop = ObjectAnimator.ofFloat(mCurrentRect, "top", mCurrentRect.top, mCurrentRect.top-translateY);
    ObjectAnimator animateBottom = ObjectAnimator.ofFloat(mCurrentRect, "bottom", mCurrentRect.bottom, mCurrentRect.bottom+translateY);
    ArgbEvaluator evaluator = new ArgbEvaluator();
    ObjectAnimator animateColor = ObjectAnimator.ofObject(this, "color", evaluator, startColor,midColor,endColor);
    animateBottom.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        //TODO: DEBUG ANIMATOR
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            postInvalidate();
        }
    });
    AnimatorSet rectAnimation = new AnimatorSet();
    rectAnimation.playTogether(animateLeft, animateRight, animateTop, animateBottom,animateColor);
    rectAnimation.setDuration(1000).start();
    invalidate();


}

The playNote, playSound and generateColor methods

    //Generates color based on the press x and y.
private int generateColor(float pressx, float pressy){
    int Red = (((int) (pressx%255)) << 16) & 0x00FF0000; //Shift red 16-bits and mask out other stuff
    int Green = (((int) (pressy%255)) << 8) & 0x0000FF00; //Shift Green 8-bits and mask out other stuff
    int Blue = ((int)((pressx+pressy)%255)) & 0x000000FF; //Mask out anything not blue.

    return 0xFF000000 | Red | Green | Blue; //0xFF000000 for 100% Alpha. Bitwise OR everything together.

}

private void playSound(String noun){
    Resources res = context.getResources();
    int resID = res.getIdentifier(noun, "raw", context.getPackageName());
    MediaPlayer mediaPlayer = MediaPlayer.create(context,resID);
    mediaPlayer.start();
    mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
        @Override
        public void onCompletion(MediaPlayer mediaPlayer) {
            mediaPlayer.release();
        }
    });
}

private void playNote(float pressx, float pressy, String note){
    colorPaint=new Paint();
    colorPaint.setStyle(Paint.Style.STROKE);
    colorPaint.setStrokeWidth(10);
    colorPaint.setColor(generateColor(pressx,pressy));
    float translateX = 50.0f;
    float translateY = 50.0f;
    Random rand = new Random(10000);
    int xr = rand.nextInt();
    int yr = rand.nextInt();
    int startColor = generateColor(pressx,pressy);
    int midColor = generateColor(pressx+rand.nextInt(),pressy+rand.nextInt());
    int endColor = generateColor(pressx+xr,pressy+yr);
    mCurrentRect = new AnimatableRectF(pressx-50,pressy-50,pressx+50,pressy+50);
    debugx=pressx;
    playAnimation(startColor,midColor,endColor, translateX, translateY);
    playSound(note);
}

Thank you!


Solution

  • Instead of using a while loop to busy wait the amount of time needed before playing the next note, best to use e.g. Timer or handler.postDelayed(runnable, delayMilliseconds), or even better, use Rx's Observable.timer().

    This is because a blocking loop prevents the UI thread from running, and therefore prevents any screen updates. The easiest way if you don't want to learn multitasking is to do:

    Handler mainHandler = new Handler(context.getMainLooper());
    
    mainHandler.postDelayed(new Runnable() {
      public void run() { playNote(...) }
    }, Times.get(i))