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

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;
    }
}
原因分析
  1. 视图测量时机错误:在视图刚添加到布局时就调用getWidth(),此时视图还未完成测量布局流程,返回的宽度是0或临时错误值,后续用这个错误值计算动画参数,导致矩形长度异常。
  2. 布局未及时刷新:设置ConstraintSet后没有强制触发视图重绘,导致视图宽度无法及时更新为预期的百分比宽度。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 15:44:50