Search code examples
javaandroidandroid-animationandroid-button

How to change the color of a view in Android as long as a repeatListener button is pressed without flickering


I have the following Java code

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
     */

    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();

        //Define the repeatListener
        binding.repeatButton.setOnTouchListener((View.OnTouchListener) new RepeatListener(30, 30, new View.OnClickListener() {

            @Override
            public void onClick(View view) {
                View_Game_Event_Rectangle activeElement = null;
                for (int currentElement =0; currentElement <arrayList_GameEventRectangles.size(); currentElement++) {
                    activeElement = arrayList_GameEventRectangles.get(currentElement);
                }

                if (activeElement!=null) {
                    activeElement.setBackground(ContextCompat.getDrawable(getActivity(),R.drawable.game_event_rectangle_solar_2).mutate());
                    // Schedule the change back to R.drawable.game_event_rectangle_solar_1 after 10 milliseconds
                    Handler handler = new Handler();
                    View_Game_Event_Rectangle finalActiveElement = activeElement;
                    handler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            // Change the background back to R.drawable.game_event_rectangle_solar_1
                            finalActiveElement.setBackground(ContextCompat.getDrawable(getActivity(), R.drawable.game_event_rectangle_solar_1).mutate());
                        }
                    }, 300);
                }



            }
        }));


        startGame();
        return binding.getRoot();
    }

    public void startGame () {
        startRound();
    }

    public void startRound () {
        orangeRectangleValuesForTheLevel = new int[5000];
        orangeRectangleValuesForTheLevel [54] = 20;



        //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_solar_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);
    }
}

enter image description here

Here is the code for the RepeatListener:

package com.example.game;

import android.os.Handler;
import android.view.MotionEvent;
import android.view.View;

/**
 * A class, that can be used as a TouchListener on any view (e.g. a Button).
 * It cyclically runs a clickListener, emulating keyboard-like behaviour. First
 * click is fired immediately, next one after the initialInterval, and subsequent
 * ones after the normalInterval.
 *
 * <p>Interval is scheduled after the onClick completes, so it has to run fast.
 * If it runs slow, it does not generate skipped onClicks. Can be rewritten to
 * achieve this.
 */
public class RepeatListener implements View.OnTouchListener {

    private Handler handler = new Handler();

    private int initialInterval;
    private final int normalInterval;
    private final View.OnClickListener clickListener;
    private View touchedView;

    private Runnable handlerRunnable = new Runnable() {
        @Override
        public void run() {
            if(touchedView.isEnabled()) {
                handler.postDelayed(this, normalInterval);
                clickListener.onClick(touchedView);
            } else {
                // if the view was disabled by the clickListener, remove the callback
                handler.removeCallbacks(handlerRunnable);
                touchedView.setPressed(false);
                touchedView = null;
            }
        }
    };

    /**
     * @param initialInterval The interval after first click event
     * @param normalInterval The interval after second and subsequent click
     *       events
     * @param clickListener The OnClickListener, that will be called
     *       periodically
     */
    public RepeatListener(int initialInterval, int normalInterval,
                          View.OnClickListener clickListener) {
        if (clickListener == null)
            throw new IllegalArgumentException("null runnable");
        if (initialInterval < 0 || normalInterval < 0)
            throw new IllegalArgumentException("negative interval");

        this.initialInterval = initialInterval;
        this.normalInterval = normalInterval;
        this.clickListener = clickListener;
    }

    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (motionEvent.getAction()) {
            case MotionEvent.ACTION_DOWN:
                handler.removeCallbacks(handlerRunnable);
                handler.postDelayed(handlerRunnable, initialInterval);
                touchedView = view;
                touchedView.setPressed(true);
                clickListener.onClick(view);
                return true;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                handler.removeCallbacks(handlerRunnable);
                touchedView.setPressed(false);
                touchedView = null;
                return true;
        }

        return false;
    }

}

What happens is that a simple custom made view rectangle View_Game_Event_Rectangle is animated from the right to the left.Then I have a repeat listener. When the repeat listener is pressed the background color of the custom view is changed from its original one R.drawable.game_event_rectangle_solar_1 to a new one R.drawable.game_event_rectangle_solar_2. A handler then changes the color back after some time. The problem is, that the custom made view rectangle flickers while holding the repeatListener pressed. I tried to change the time delay for the handler but this did not solve the problem (when the delay is too high, the color remains changed for a long time). What I want is that the color of the custom made view rectangle keeps changed as long as the repeatListener button is pressed. After the repeatListener button is not pressed any more, the color should change back to its original one. The improtant aspect is that while the button is pressed, the custom made view rectangle should not flicker but just have the alternative color R.drawable.game_event_rectangle_solar_2.

Please: Please can someone share his/her idea? I'd be quite thankful.


Solution

  • What I want is that the color of the custom made view rectangle keeps changed as long as the repeatListener button is pressed.

    Color of you rectangle is changed back after 300ms, thats what you have coded. This might give a flicker.

    Its hard to tell from the provided code what is exactly wrong. You have not provided code for RepeatListener. I suspect that the code inside:

    public void onClick(View view) {
    

    gets executed even after first time button was touched. You should execute your on touch code when event.getAction() == MotionEvent.ACTION_DOWN and when its event.getAction() == MotionEvent.ACTION_UP, handle touch up action. Otherwise you will intercept also MotionEvent.ACTION_MOVE events which can give you flicker. I assume this is not implemented in RepeatListener.

    So, when MotionEvent.ACTION_DOWN change color to the one you want to stay for the time user touches button. Then when MotionEvent.ACTION_UP comes, change the color back. I dont see a need for postDelayed here.