Search code examples
javaandroidanimationimageviewontouchlistener

How to combine a RepeatListener in Android with a smooth image rotation


I have a Fragment with a ImageView (Fan) and an action button. When pressing the action button the ImageView should rotate by using the animation approach from How to make a smooth image rotation in Android?. Here is the screenshot of the layout: enter image description here

Actually this works fine when I just press the button once and release it. However, when having a RepeatListener and pressing and holding it there are 2 problems:

  1. The binding.fan.startAnimation(rotate); is called several times within a short time. This leads to setting back the ImageView such that it does not seem to animate until the button is not pressed any more
  2. When the button is not pressed any more, the ImageView should stay at the very position that it has had when the button was released. So it should not turn back to its original state.

Can you think of a approach to archieve this?

Here is the Java code of the Fragment with the RepeatListener

package com.example.game;

import android.content.pm.ActivityInfo;
import android.graphics.Point;
import android.os.Bundle;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import androidx.fragment.app.Fragment;
import com.example.game.databinding.FragmentTest2Binding;



public class Test2 extends Fragment implements View.OnClickListener {


    private FragmentTest2Binding binding;
    int widthDisplay;
    int heightDisplay;

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


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);


    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        binding = FragmentTest2Binding.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();

        //Define the rotation animation
        RotateAnimation rotate = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
        rotate.setDuration(1000);
        rotate.setInterpolator(new LinearInterpolator());
        rotate.setFillAfter(true);
        binding.fan.setAnimation(rotate);


        // Define and registrate RepeatListener on the action button


        binding.buttonAction.setOnTouchListener((View.OnTouchListener) new RepeatListener(30, 30, new View.OnClickListener() {

            @Override
            public void onClick(View view) {

                binding.fan.startAnimation(rotate);
                // the code to execute repeatedly

            }
        }));


        return binding.getRoot();

    }

    @Override
    public void onClick(View v) {

    }
}

This is the XML layout file:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/game_test_background_hp_fan"
    tools:context=".MainActivity"
    android:id="@+id/constraintLayout">


    <Button
        android:id="@+id/button_action"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHeight_percent="0.102"
        app:layout_constraintHorizontal_bias="0.373"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.745"
        app:layout_constraintWidth_percent="0.12" />

    <ImageView
        android:id="@+id/fan"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHeight_percent="0.09"
        app:layout_constraintHorizontal_bias="0.539"
        app:layout_constraintVertical_bias= "0.51"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/heat_pump_fan" />
</androidx.constraintlayout.widget.ConstraintLayout>

Edit: Here is the java class of the RepeatListener:

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;
    }

}

Reminder: Does anyone kow how to do this?


Solution

  • The intend of your linked post

    How to make a smooth image rotation in Android?

    is to rotate a view forever but what you want is a custom rotation. You can rotate any view using setRotation(float angle). Make sure that the view has free space for rotating.

    public class Test2 extends Fragment implements View.OnClickListener {
    
    float rotationAngle = 0; // <------- added
    ...
    
    ...
    
    @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {
            binding = FragmentTest2Binding.inflate(inflater, container, false);
    
            // Define and registrate RepeatListener on the action button
    
    
            binding.buttonAction.setOnTouchListener(new RepeatListener(30, 30, view -> {
                rotationAngle += 5;
                binding.fan.setRotation(-rotationAngle); // I want to rotate counter-clock-wise
    
            }));
    
    
            return binding.getRoot();
    
        }
    }
    

    enter image description here