Android ConstraintLayout中自定义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动画配合百分比约束时,全局约束修改会干扰视图尺寸和绘制区域,加剧问题。
解决步骤
为每个视图单独创建ConstraintSet
不要复用全局constraintSet,创建新视图时新建独立的ConstraintSet配置约束,避免影响已存在的视图:// 创建视图的代码块中替换原有逻辑 ConstraintSet newConstraintSet = new ConstraintSet(); newConstraintSet.clone(constraintLayout); // 配置当前视图的约束... newConstraintSet.applyTo(constraintLayout);提前创建视图并隐藏,避免动态添加冲突
在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"/>控制整个视图的透明度,而非仅背景
替换getBackground().setAlpha()为View.setAlpha(),确保图标和矩形同步显示/隐藏:// 隐藏时 arrayList_GameEventRectangles.get(currentElement).setAlpha(0f); // 显示时 arrayList_GameEventRectangles.get(currentElement).setAlpha(1f);使用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

