You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android ConstraintLayout中自定义UI元素动画时变形问题求助

问题:Android自定义UI元素动画时图标消失变形

我需要在Android中实现一个带图标的矩形自定义UI元素,让它从屏幕右侧向左流动。但遇到一个问题:当第一个UI元素正在执行动画时创建第二个UI元素,第一个元素会变形——矩形内的图标消失,只剩橙色矩形继续向左移动。

两张截图显示,原本带有图标的流动矩形,在第二个UI创建后,图标离奇消失,仅保留橙色圆角矩形持续移动。

我采用Stack Overflow上自定义UI的XML方案,使用Test Fragment结合CountDownTimer,从View_Game_Event_Rectangle类创建UI对象。流程如下:

  • 提前50个时间槽创建UI元素并设为不可见
  • 提前40个时间槽将元素移至屏幕最右侧
  • 到达预定时间槽时,启动从右向左的动画并设置元素可见

单独操作时一切正常,但在第一个元素动画期间创建第二个元素就会出现变形问题。请问如何确保矩形和图标始终保持在一起?


自定义UI的XML布局

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape android:shape="rectangle">
            <corners android:radius="5dp" />
            <solid android:color="#FF9800" />
        </shape>
    </item>
    <item
        android:drawable="@drawable/ic_launcher_light"
        android:width="48dp"
        android:height="48dp"
        android:gravity="left|center_vertical" />
</layer-list>

Test Fragment代码

public class Test extends Fragment {


    /*
    Game variables
     */

    public static final int DELAY_IN_MILLIS = 100;
    public static final double DIFFICULTY_SPEED_MULTIPLICATOR_INCREMENT_PER_LEVEL = 0.3;
    public static final int TIME_OF_A_LEVEL_IN_SECONDS = 90;

    private int currentTimeLeftInTheLevel_MILLIS;
    private int currentTimeSlot;
    private boolean gameIsRunning;
    private Handler handler = new Handler();

    private double diffcultySpeedMultiplicator;

    private int timeslotsEventElementUntilTheMiddleOfTheScreen_Level1 = (int)(5.0 * (1000.0/DELAY_IN_MILLIS));
    private int numberOfMillisecondsUntilTheMiddleOfTheScreen_Level1 = 3000;



    //Fix parameters of the UI elements
    private float horizontalPercentageOfScoreRectangle_End = 0.491f;
    private float horizontalPercentageOfScoreRectangle_Start = 0.567f;

    private float verticalBiasOfEventElementToBeInTheLine = 0.049f;
    private float percentageHeightOfEventElement = 0.071f;


    int widthDisplay;
    int heightDisplay;

    int helpCounterRun =0;
    int helpCounterUpdateScreen =0;
    int helpCounterCountDownTime =0;


    private FragmentTestBinding binding;

    private ConstraintLayout constraintLayout;
    ConstraintSet constraintSet ;

    //Variables for the single view event
    View_Game_Event_Rectangle[] viewEvent;
    Drawable[] drawingsForTheViewEvents;
    int [] startingTimeSlotViewEvent;
    int [] durationViewEvent;
    ArrayList<View_Game_Event_Rectangle> arrayList_GameEventRectangles;


    private boolean fragmentViewHasBeenCreated = false;


    private CountDownTimer cdt;
    private  final long DURATION = 900000L; //40sec
    private  final long DELAY_COUNT_DOWN_TIMER = 50L; //100ms

    private int numberOfTimeSlotsUntilTheEndOfScreen = (int)(numberOfMillisecondsUntilTheMiddleOfTheScreen_Level1 * 2/(DELAY_COUNT_DOWN_TIMER));



    public Test() {
        // Required empty public constructor
    }


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //Set the startingTimeSlots and the durations of the 2 custom UI elements
        viewEvent = new View_Game_Event_Rectangle[2];
        drawingsForTheViewEvents = new Drawable[2];
        startingTimeSlotViewEvent = new int [2];
        durationViewEvent = new int [2];
        startingTimeSlotViewEvent [0] = 51;
        durationViewEvent [0] = 10;
        startingTimeSlotViewEvent [1] = 111;
        durationViewEvent [1] = 1;

        arrayList_GameEventRectangles = new ArrayList<View_Game_Event_Rectangle>();


    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        binding = FragmentTestBinding.inflate(inflater, container, false);

        WindowManager wm = (WindowManager) getActivity().getWindowManager();
        Display display = wm.getDefaultDisplay();
        Point size = new Point();
        display.getSize(size);
        widthDisplay = size.x;
        heightDisplay = size.y;

        container.getContext();
        constraintLayout= binding.constraintLayout;

        fragmentViewHasBeenCreated = true;
        getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        constraintLayout = binding.constraintLayout;
        constraintSet = new ConstraintSet();
        startGame();
        return binding.getRoot();



    }


    public void startGame () {
        gameIsRunning = true;
        startRound();
    }

    public void startRound () {
        Log.e("LogTag_El", "-------Start Test Game------" );
        diffcultySpeedMultiplicator = 1;
        currentTimeSlot =0;

        //Create the array list with the Game_Event_Rectangles
        arrayList_GameEventRectangles.add(new View_Game_Event_Rectangle(getActivity(), "Bulb 1", startingTimeSlotViewEvent [0], durationViewEvent [0]));
        arrayList_GameEventRectangles.add(new View_Game_Event_Rectangle(getActivity(), "Bulb 1", startingTimeSlotViewEvent [1], durationViewEvent [1]));
        countDownTime();

    }

    //Method for updating the screen iteratively
    private void updateScreen() {

        //Iterate through all custom UI objects in the arrayList_GameEventRectangles in each time slot and see if an action has to be taken
        for (int currentElement =0; currentElement <arrayList_GameEventRectangles.size(); currentElement++) {

            //Create view, set its background and define the constraints for the Constraint Layout (50 timeslots before its starting TimeSlot)
            if (currentTimeSlot == arrayList_GameEventRectangles.get(currentElement).getStartingTimeSlot() - 50) {
                Log.e("LogTag_El", "Create view - currentTimeSlot: " + currentTimeSlot);
                Log.e("LogTag_El", "Create view - currentElement: " + currentElement);
                Log.e("LogTag_El", "Create view - getDuration: " + arrayList_GameEventRectangles.get(currentElement).getDuration());
                Log.e("LogTag_El", "Create view - getStartingTimeSlots: " + arrayList_GameEventRectangles.get(currentElement).getStartingTimeSlot());


                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));
                arrayList_GameEventRectangles.get(currentElement).setBackground(ContextCompat.getDrawable(getActivity(),R.drawable.game_event_rectangle_bulb_1));


                arrayList_GameEventRectangles.get(currentElement).setId(View.generateViewId());

                //Make the view invisible (before it's appearence time)
                arrayList_GameEventRectangles.get(currentElement).getBackground().setAlpha(0);

                // Set the ConstraintLayout programatically for the view
                constraintLayout.addView(arrayList_GameEventRectangles.get(currentElement));
                constraintSet.clone(constraintLayout);
                constraintSet.constrainPercentHeight(arrayList_GameEventRectangles.get(currentElement).getId(), percentageHeightOfEventElement);

                float widthConstrainPercentage_element1 = (float)(arrayList_GameEventRectangles.get(currentElement).getDuration() / ((double) numberOfTimeSlotsUntilTheEndOfScreen));
                float duration = arrayList_GameEventRectangles.get(currentElement).getDuration();

                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);
                constraintSet.setVerticalBias(arrayList_GameEventRectangles.get(currentElement).getId(), verticalBiasOfEventElementToBeInTheLine);
                constraintSet.applyTo(constraintLayout);

            }


            //Shift the view to the right border of the display (40 timeslots before its starting TimeSlot)
            if (currentTimeSlot == arrayList_GameEventRectangles.get(currentElement).getStartingTimeSlot() - 40) {
                Log.e("LogTag_El", "Shift view - currentTimeSlot: " + currentTimeSlot);
                Log.e("LogTag_El", "Shift view - currentElement: " + currentElement);
                arrayList_GameEventRectangles.get(currentElement).setTranslationX(arrayList_GameEventRectangles.get(currentElement).getWidth());
            }


            //Animate view element from right to left on the screen and make the background visible (when its startingTimeSlot has come)
            if (currentTimeSlot ==arrayList_GameEventRectangles.get(currentElement).getStartingTimeSlot()) {
                Log.e("LogTag_El", "Animate view - currentTimeSlot: " + currentTimeSlot);
                Log.e("LogTag_El", "Animate view - currentElement: " + currentElement);
                arrayList_GameEventRectangles.get(currentElement).getBackground().setAlpha(255);
                arrayList_GameEventRectangles.get(currentElement).animate().setDuration(numberOfMillisecondsUntilTheMiddleOfTheScreen_Level1).translationX(widthDisplay*(-1)).start();

            }

            helpCounterUpdateScreen++;


        }
    }

    //Method for counting down the time using the thread handling class "CountDownTimer"
    private void countDownTime(){
        cdt = new CountDownTimer(DURATION, DELAY_COUNT_DOWN_TIMER) {
            boolean delay = true;
            public void onTick(long millisUntilFinished) {
                if(delay) {
                    delay = false;
                } else {
                    currentTimeLeftInTheLevel_MILLIS = currentTimeLeftInTheLevel_MILLIS - DELAY_IN_MILLIS;
                    helpCounterCountDownTime++;
                    currentTimeSlot++;

                    updateScreen();
                    delay = true;
                }
            }
            public void onFinish() {
                updateScreen();
            }
        }.start();

    }


    @Override
    public void onDestroyView() {
        super.onDestroyView();

        // Reset your variable to false
        fragmentViewHasBeenCreated = false;

        // And clean up any postDelayed callbacks that are waiting to fire
        cdt.cancel();
        handler.removeCallbacksAndMessages(null);
    }
}

问题原因与解决方法

核心原因

问题出在复用同一个ConstraintSet对象处理所有视图:每次创建新UI元素时,constraintSet.clone(constraintLayout)会复制当前所有视图的约束,随后applyTo会将这些约束重新应用到布局中,导致正在动画的视图背景Drawable被重新初始化,约束被覆盖,最终图标丢失。此外,translationX动画配合百分比约束时,全局约束修改会干扰视图尺寸和绘制区域,加剧问题。

解决步骤

  1. 为每个视图单独创建ConstraintSet
    不要复用全局constraintSet,创建新视图时新建独立的ConstraintSet配置约束,避免影响已存在的视图:

    // 创建视图的代码块中替换原有逻辑
    ConstraintSet newConstraintSet = new ConstraintSet();
    newConstraintSet.clone(constraintLayout);
    // 配置当前视图的约束...
    newConstraintSet.applyTo(constraintLayout);
    
  2. 提前创建视图并隐藏,避免动态添加冲突
    在Fragment布局XML中提前定义所有需要的UI元素,设置android:visibility="gone",到时间再显示并启动动画:

    <com.yourpackage.View_Game_Event_Rectangle
        android:id="@+id/event_view_1"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:visibility="gone"
        app:layout_constraintHeight_percent="0.071"
        app:layout_constraintWidth_percent="0.1"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintVertical_bias="0.049"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>
    
  3. 控制整个视图的透明度,而非仅背景
    替换getBackground().setAlpha()为View.setAlpha(),确保图标和矩形同步显示/隐藏:

    // 隐藏时
    arrayList_GameEventRectangles.get(currentElement).setAlpha(0f);
    // 显示时
    arrayList_GameEventRectangles.get(currentElement).setAlpha(1f);
    
  4. 使用ObjectAnimator替代View.animate()
    用更稳定的ObjectAnimator控制动画,避免布局修改打断动画过程:

    View view = arrayList_GameEventRectangles.get(currentElement);
    ObjectAnimator animator = ObjectAnimator.ofFloat(view, "translationX", view.getWidth(), -widthDisplay);
    animator.setDuration(numberOfMillisecondsUntilTheMiddleOfTheScreen_Level1);
    animator.start();
    

内容的提问来源于stack exchange,提问作者VanessaF

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 23:18:24