3

I'm developing an Android game that animates rectangular views moving horizontally across the screen using View.animate().translationX(...). The animation is just a plain rectangle moving left over a few seconds, yet it consistently lags on older devices (Samsung S7 and old tablets) and even slightly on newer ones in power-saving mode (Samsung S22 Ultra).

Stripped-down version:

View rectangle = ...; // A dynamically added View to a ConstraintLayout
rectangle.animate()
 .setDuration(6000)
 .translationX(-widthDisplay)
 .setInterpolator(new LinearInterpolator())
 .start();

I’ve tried the following optimizations, but the lag persists:

  • Using View.setLayerType(View.LAYER_TYPE_HARDWARE, null).
  • Reducing the number of simultaneously animated views to just one.
  • Avoiding any other heavy processing on the UI thread.
  • Using ObjectAnimator instead.

The issue is not related to the view complexity, number of views, or background tasks. Even animating a single rectangle causes lag on older or throttled devices. Removing setAlpha() or similar calls doesn't help.

Is there a performance bottleneck with View.animate() (ViewPropertyAnimator) on older or throttled Android devices? And if so, is there a more efficient alternative or workaround for smooth, long-duration translation animations?

My code for setting up the rectangles and animating them:

for (int currentElement =0; currentElement <arrayList_GameEventRectangles.size(); currentElement++) {
 //Create view and set
 if (currentTimeSlot == arrayList_GameEventRectangles.get(currentElement).getStartingTimeSlot() - 15) {
 arrayList_GameEventRectangles.get(currentElement).setActive(true);
 //Set the parameters and the background of the view element
 arrayList_GameEventRectangles.get(currentElement).setLayoutParams(new ViewGroup.LayoutParams(0, 0));
 if(arrayList_GameEventRectangles.get(currentElement).getEventType().equals(VIEW_EVENT_RECTANGLE_SOLAR)) {
 arrayList_GameEventRectangles.get(currentElement).setBackground(Objects.requireNonNull(ContextCompat.getDrawable(requireActivity(), R.drawable.game_event_rectangle_solar_1)).mutate());
 }
 if(arrayList_GameEventRectangles.get(currentElement).getEventType().equals(VIEW_EVENT_RECTANGLE_WIND)) {
 arrayList_GameEventRectangles.get(currentElement).setBackground(Objects.requireNonNull(ContextCompat.getDrawable(requireActivity(), R.drawable.game_event_rectangle_wind_1)).mutate());
 }
 if(arrayList_GameEventRectangles.get(currentElement).getEventType().equals(VIEW_EVENT_RECTANGLE_GAS)) {
 arrayList_GameEventRectangles.get(currentElement).setBackground(Objects.requireNonNull(ContextCompat.getDrawable(requireActivity(), R.drawable.game_event_rectangle_gas_1)).mutate());
 }
 if(arrayList_GameEventRectangles.get(currentElement).getEventType().equals(VIEW_EVENT_RECTANGLE_COAL)) {
 arrayList_GameEventRectangles.get(currentElement).setBackground(Objects.requireNonNull(ContextCompat.getDrawable(requireActivity(), R.drawable.game_event_rectangle_coal_1)).mutate());
 }
 arrayList_GameEventRectangles.get(currentElement).setId(View.generateViewId());
 //Make the view invisible (before it's appearance time)
 arrayList_GameEventRectangles.get(currentElement).getBackground().setAlpha(0);
 constraintLayout.bringChildToFront(binding.imageViewTargetRectangle);
 // Set the ConstraintLayout programmatically for the view
 View view = arrayList_GameEventRectangles.get(currentElement);
 ViewGroup parent = (ViewGroup) view.getParent();
 // Check if the view already has a parent
 if (parent != null) {
 parent.removeView(view); // Remove the view from its existing parent
 }
 constraintLayout.addView(view); // Now add the view to the constraintLayout
 constraintSet.clone(constraintLayout);
 float percentageHeightOfEventElement = 0.071f;
 constraintSet.constrainPercentHeight(arrayList_GameEventRectangles.get(currentElement).getId(), percentageHeightOfEventElement);
 float widthConstrainPercentage_element1 = (float)(arrayList_GameEventRectangles.get(currentElement).getDuration() / 100.0);
 constraintSet.constrainPercentWidth(arrayList_GameEventRectangles.get(currentElement).getId(), widthConstrainPercentage_element1);
 constraintSet.connect(arrayList_GameEventRectangles.get(currentElement).getId(),ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID,ConstraintSet.BOTTOM,0);
 constraintSet.connect(arrayList_GameEventRectangles.get(currentElement).getId(),ConstraintSet.TOP,ConstraintSet.PARENT_ID ,ConstraintSet.TOP,0);
 constraintSet.connect(arrayList_GameEventRectangles.get(currentElement).getId(),ConstraintSet.LEFT,ConstraintSet.PARENT_ID ,ConstraintSet.LEFT,0);
 constraintSet.connect(arrayList_GameEventRectangles.get(currentElement).getId(),ConstraintSet.RIGHT,ConstraintSet.PARENT_ID ,ConstraintSet.RIGHT,0);
 float horizontalBias = 1.0f ;
 constraintSet.setHorizontalBias(arrayList_GameEventRectangles.get(currentElement).getId(), horizontalBias);
 float verticalBiasOfEventElementToBeInTheLine = 0.049f;
 constraintSet.setVerticalBias(arrayList_GameEventRectangles.get(currentElement).getId(), verticalBiasOfEventElementToBeInTheLine);
 constraintSet.applyTo(constraintLayout);
 }
 //Shift the view to the right border of the display. This is done before the view is being displayed to the user such that it can flow from right to left in the game
 if (currentTimeSlot == arrayList_GameEventRectangles.get(currentElement).getStartingTimeSlot() - 10) {
 arrayList_GameEventRectangles.get(currentElement).setTranslationX(arrayList_GameEventRectangles.get(currentElement).getWidth());
 }
 //Animate view element
 if (currentTimeSlot == arrayList_GameEventRectangles.get(currentElement).getStartingTimeSlot()) {
 arrayList_GameEventRectangles.get(currentElement).getBackground().setAlpha(255);
 View rectangle = arrayList_GameEventRectangles.get(currentElement);
 int rectangleWidth = rectangle.getWidth();
 float distanceToCover_current = widthDisplay + rectangleWidth;
 float distanceToCover_normalizedObject = widthDisplay + 20;
 double ratioDistanceDifference = distanceToCover_current /distanceToCover_normalizedObject;
 int numberOfMillisecondsUntilTheMiddleOfTheScreen_Level1 = 8000;
 long durationForTheAnimation = (long)(numberOfMillisecondsUntilTheMiddleOfTheScreen_Level1 * speedMultiplicatorLevel [currentLevel - 1] * ratioDistanceDifference);
 arrayList_GameEventRectangles.get(currentElement).animate().setDuration(durationForTheAnimation).translationX(widthDisplay*(-1)).setInterpolator(new LinearInterpolator()).start();
 }
 helpCounterUpdateScreen++;
 //Check if the view is still running
 if (arrayList_GameEventRectangles.get(currentElement).isActive() && currentElement==0) {
 if (arrayList_GameEventRectangles.get(currentElement).getX() < arrayList_GameEventRectangles.get(currentElement).getWidth() * (-0.8)) {
 arrayList_GameEventRectangles.get(currentElement).incrementNumberOfTimeSlotsAfterFinishing();
 }
 if (arrayList_GameEventRectangles.get(currentElement).getNumberOfTimeSlotsAfterFinishing()> 100) {
 arrayList_GameEventRectangles.get(currentElement).setActive(false);
 }
 }
}

Reminder: Does anyone have another idea without SurfaceView?

asked Jul 18, 2025 at 8:01
0

1 Answer 1

3
+25

Using SurfaceView with the same logic of rectangles and animating them:

GameSurfaceView class:

package com.example.surfaceanimationtest;
import android.content.Context;
import android.content.res.Resources;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
public class GameSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
 private List<MyGameRect> rectangles = new ArrayList<>();
 private MyDrawThread mMyDrawThread;
 public GameSurfaceView(Context context) {
 super(context);
 getHolder().addCallback(this);
 }
 private void init() {
 getHolder().addCallback(this);
 int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
 rectangles = new ArrayList<>();
 mMyDrawThread = new MyDrawThread(getHolder(), rectangles, screenWidth);
 }
 public GameSurfaceView(Context context, AttributeSet attrs) {
 super(context, attrs);
 init();
 }
 @Override
 public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) {
 }
 @Override
 public void surfaceCreated(@NonNull SurfaceHolder holder) {
 mMyDrawThread = new MyDrawThread(getHolder(), rectangles, getWidth());
 mMyDrawThread.start();
 }
 @Override
 public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
 if (mMyDrawThread != null) {
 mMyDrawThread.stopDrawing();
 }
 }
 public void addRectangle() {
 MyGameRect rect = new MyGameRect();
 rect.x = getWidth(); // from right
 rect.y = getHeight() * 0.05f;
 rect.width = getWidth() * 0.1f;
 rect.height = getHeight() * 0.07f;
 rect.startTime = System.currentTimeMillis();
 rect.duration = 6000; // ms
 synchronized (rectangles) {
 rectangles.add(rect);
 }
}
}

MyGameRect class:

public class MyGameRect {
 float x, y, width, height;
 long startTime;
 long duration;
 boolean isFinished() {
 return (System.currentTimeMillis() - startTime) > duration;
 }
 void updatePosition(float screenWidth) {
 float elapsed = System.currentTimeMillis() - startTime;
 float progress = Math.min(1f, elapsed / (float) duration);
 x = screenWidth - progress * (screenWidth + width);
 }
}

MyDrawThread class:

public class MyDrawThread extends Thread {
 private final SurfaceHolder holder;
 private final List<MyGameRect> rectangles;
 private final float screenWidth;
 private boolean running = true;
 public MyDrawThread(SurfaceHolder holder, List<MyGameRect> rectangles, float screenWidth) {
 this.holder = holder;
 this.rectangles = rectangles;
 this.screenWidth = screenWidth;
 }
 public void stopDrawing() {
 running = false;
 interrupt();
 }
 @Override
 public void run() {
 Paint paint = new Paint();
 paint.setColor(Color.YELLOW);
 while (running) {
 Canvas canvas = holder.lockCanvas();
 if (canvas == null) continue;
 canvas.drawColor(Color.BLACK);
 synchronized (rectangles) {
 Iterator<MyGameRect> iterator = rectangles.iterator();
 while (iterator.hasNext()) {
 MyGameRect rect = iterator.next();
 rect.updatePosition(screenWidth);
 if (rect.isFinished()) {
 iterator.remove();
 continue;
 }
 canvas.drawRect(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height, paint);
 }
 }
 holder.unlockCanvasAndPost(canvas);
 try {
 sleep(16); // about 60 FPS
 } catch (InterruptedException ignored) {}
 }
 }
}

Setting view in main activity XML or your fragment:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:app="http://schemas.android.com/apk/res-auto"
 xmlns:tools="http://schemas.android.com/tools"
 android:id="@+id/main"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 tools:context=".MainActivity">
 <com.example.surfaceanimationtest.GameSurfaceView
 android:id="@+id/gameSurfaceView"
 android:layout_width="0dp"
 android:layout_height="0dp"
 app:layout_constraintTop_toTopOf="parent"
 app:layout_constraintBottom_toBottomOf="parent"
 app:layout_constraintStart_toStartOf="parent"
 app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity:

package com.example.surfaceanimationtest;
import android.os.Bundle;
import android.os.Handler;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class MainActivity extends AppCompatActivity {
 GameSurfaceView gameSurfaceView;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 EdgeToEdge.enable(this);
 setContentView(R.layout.activity_main);
 ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
 Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
 v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
 return insets;
 });
 gameSurfaceView = findViewById(R.id.gameSurfaceView);
 // this will add rectangles every 1.5 seconds
 new Handler().postDelayed(new Runnable() {
 @Override
 public void run() {
 gameSurfaceView.addRectangle();
 gameSurfaceView.postDelayed(this, 1500);
 }
 }, 1000);
 }
}

The result on Google Pixel Emulator 2 is similar to the Samsung Galaxy S7:

Add android:hardwareAccelerated="true" in AndroidManifest.xml. This enables hardware acceleration which can significantly improve animation performance on supported devices.

As an alternative to a raw thread consider RxJava for thread management and scheduling. It provides a cleaner and more reactive approach to handle frame updates or timed animations, especially UI-related timing operations. It should have better performance on weak devices.

user4157124
2,99422 gold badges33 silver badges48 bronze badges
answered Jul 22, 2025 at 22:47
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks Dr for your answer and your tremendous help, which I highly appreciate. As my game logic in the app is storngly based on the views and their position and your suggested approach is way to complex for me to adapt it to my case, I unfortunately can't use your answer. I am more seeking for an easy solution how to improve the performance of View.animate directly without redesigning my whole code which would require quite some effort. Still I highly appreciate your help.
Just one side remark: Using an emulator to evaluate the performance is not a good option. I also made this mistake. When using an emulator, even one with very limited ressources, the animations don't lag at all while on a real device they lag strongly. The reason for this is that the emulator finally gets its computational resources from the underlyning hardware. As my computer's CPU is powerful, even an explicity designed emulator on Android Studio with very low performance parameters let's the animation flow smoothly. As a matter of fact I also used an emulator of Pixel2_Low_Performance
Thank you for your feedback, btw I tested my solution on my real device (old Xiaomi 2018 smartphone), and its working flawlessly without this problem, anyway I hope you find another solution fitting with your scinario

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.