Android中同属性自定义视图多次运行尺寸不一致问题排查
问题描述
自定义了View_Game_Event_Rectangle视图,是带图标的橙色矩形,通过Fragment实现从屏幕右侧向左滑动的动画效果。但多次运行相同代码时,矩形长度会出现不一致的情况(见下图),所有视图属性完全一致,部分运行场景下显示正常,部分场景下显示异常。

动画Fragment类Java代码
package com.example.game; import android.content.pm.ActivityInfo; import android.graphics.Point; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.CountDownTimer; import android.os.Handler; import android.view.Display; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.animation.LinearInterpolator; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import com.example.game.databinding.FragmentTestBinding; import java.util.ArrayList; 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 Handler handler = new Handler(); private int numberOfMillisecondsUntilTheMiddleOfTheScreen_Level1 = 8000; private int currentTimeSlot; private float verticalBiasOfEventElementToBeInTheLine = 0.049f; private float percentageHeightOfEventElement = 0.071f; int widthDisplay; int heightDisplay; //Type of View_Game_Events public static final String VIEW_EVENT_RECTANGLE_SOLAR = "Solar"; private FragmentTestBinding binding; private ConstraintLayout constraintLayout; ConstraintSet constraintSet ; //Variables for the single view event View_Game_Event_Rectangle[] viewEvent; boolean [] isViewEventActive; Drawable[] drawingsForTheViewEvents; private static int nextFreeIndexForViewEvent; private static int numberOfViewEventInArray = 10; ArrayList<View_Game_Event_Rectangle> arrayList_GameEventRectangles; private int [] orangeRectangleValuesForTheLevel; private boolean fragmentViewHasBeenCreated = false; private CountDownTimer cdt; private final long DELAY_COUNT_DOWN_TIMER = 100; //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); nextFreeIndexForViewEvent = 0; viewEvent = new View_Game_Event_Rectangle[numberOfViewEventInArray]; drawingsForTheViewEvents = new Drawable[numberOfViewEventInArray]; arrayList_GameEventRectangles = new ArrayList<View_Game_Event_Rectangle>(); isViewEventActive = new boolean[numberOfViewEventInArray]; } @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 () { startRound(); } public void startRound () { orangeRectangleValuesForTheLevel = new int[5000]; orangeRectangleValuesForTheLevel [74] = 25; orangeRectangleValuesForTheLevel [83] = 25; orangeRectangleValuesForTheLevel [92] = 25; orangeRectangleValuesForTheLevel [101] = 25; //Create the array list with the Game_Event_Rectangles for (int i =0; i<orangeRectangleValuesForTheLevel.length; i++) { if (orangeRectangleValuesForTheLevel[i] >0) { arrayList_GameEventRectangles.add(new View_Game_Event_Rectangle(getActivity(), VIEW_EVENT_RECTANGLE_SOLAR, i+1, orangeRectangleValuesForTheLevel[i])); } } countDownTime(); } private void updateScreen() { /* Iterate through all elements */ 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 backgorund 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(ContextCompat.getDrawable(getActivity(),R.drawable.game_event_rectangle_bulb_1).mutate()); } 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() / 100.0); 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 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; long durationForTheAnimation = (long)(numberOfMillisecondsUntilTheMiddleOfTheScreen_Level1 * ratioDistanceDifference); arrayList_GameEventRectangles.get(currentElement).animate().setDuration(durationForTheAnimation).translationX(widthDisplay*(-1)).setInterpolator(new LinearInterpolator()).start(); } } } private void countDownTime(){ cdt = new CountDownTimer(100000, DELAY_COUNT_DOWN_TIMER) { boolean delay = true; public void onTick(long millisUntilFinished) { if(delay) { delay = false; } else { 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); } }
自定义视图类Java代码
package com.example.game; import android.content.Context; import android.view.View; public class View_Game_Event_Rectangle extends View { private boolean isActive; private int numberOfTimeSlotsAfterFinishing; private String eventType; private int startingTimeSlot; private int duration; public View_Game_Event_Rectangle(Context context, String eventType, int startingTimeSlot, int duration ) { super(context); this.eventType = eventType; this.startingTimeSlot = startingTimeSlot; this.duration = duration; } public boolean isActive() { return isActive; } public void setActive(boolean active) { isActive = active; } public int getNumberOfTimeSlotsAfterFinishing() { return numberOfTimeSlotsAfterFinishing; } public void setNumberOfTimeSlotsAfterFinishing(int numberOfTimeSlotsAfterFinishing) { this.numberOfTimeSlotsAfterFinishing = numberOfTimeSlotsAfterFinishing; } public void incrementNumberOfTimeSlotsAfterFinishing() { this.numberOfTimeSlotsAfterFinishing = this.numberOfTimeSlotsAfterFinishing + 1; } public String getEventType() { return eventType; } public void setEventType(String eventType) { this.eventType = eventType; } public int getStartingTimeSlot() { return startingTimeSlot; } public void setStartingTimeSlot(int startingTimeSlot) { this.startingTimeSlot = startingTimeSlot; } public int getDuration() { return duration; } public void setDuration(int duration) { this.duration = duration; } }
原因分析
- 视图测量时机错误:在视图刚添加到布局时就调用
getWidth(),此时视图还未完成测量布局流程,返回的宽度是0或临时错误值,后续用这个错误值计算动画参数,导致矩形长度异常。 - 布局未及时刷新:设置ConstraintSet后没有强制触发视图重绘,导致视图宽度无法及时更新为预期的百分比宽度。
- Fragment重建时数据残留:
arrayList_GameEventRectangles在onCreate初始化,Fragment重建时旧视图对象未清空,导致状态混乱。
解决方案
1. 避免依赖实时视图宽度,直接计算预期宽度
动画部分不要调用getWidth(),而是基于屏幕宽度和预设百分比计算预期宽度:
// 替换原动画代码中的宽度获取逻辑 float expectedWidth = widthDisplay * (arrayList_GameEventRectangles.get(currentElement).getDuration() / 100.0f); float distanceToCover_current = widthDisplay + expectedWidth; float distanceToCover_normalizedObject = widthDisplay + 20; double ratioDistanceDifference = distanceToCover_current / distanceToCover_normalizedObject; long durationForTheAnimation = (long)(numberOfMillisecondsUntilTheMiddleOfTheScreen_Level1 * ratioDistanceDifference); arrayList_GameEventRectangles.get(currentElement).animate() .setDuration(durationForTheAnimation) .translationX(-widthDisplay) .setInterpolator(new LinearInterpolator()) .start();
2. 添加视图后强制触发布局测量
在constraintSet.applyTo(constraintLayout)后添加以下代码,确保视图完成布局:
constraintLayout.requestLayout(); arrayList_GameEventRectangles.get(currentElement).post(new Runnable() { @Override public void run() { // 若后续需要获取真实宽度,可在此处执行 } });
3. 清空Fragment重建时的残留数据
在onDestroyView中清空列表,避免旧数据干扰:
@Override public void onDestroyView() { super.onDestroyView(); fragmentViewHasBeenCreated = false; cdt.cancel(); handler.removeCallbacksAndMessages(null); arrayList_GameEventRectangles.clear(); }
4. 优化视图LayoutParams初始化
改用ConstraintLayout专属LayoutParams,更符合布局体系:
// 替换原setLayoutParams代码 ConstraintLayout.LayoutParams params = new ConstraintLayout.LayoutParams( ConstraintLayout.LayoutParams.MATCH_CONSTRAINT, ConstraintLayout.LayoutParams.MATCH_CONSTRAINT ); arrayList_GameEventRectangles.get(currentElement).setLayoutParams(params);
内容的提问来源于stack exchange,提问作者VanessaF
相关产品推荐
相关产品推荐

