Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android slide to answer like ImageView animation

I need to implement an Animation on an ImageView, similar to the slide to answer animation that exists in many Android devices. The requiremnts:

  1. Support API level >= 8 ( if not possible then 9 ), so the convenient drag listener is out of the question
  2. move the ImageView right or left when dragging it, only horizontal dragging is required. At first the image is centered horizontally.
  3. Scale the ImageView while dragging - as closer as it gets to the end of the screen the smaller the image will become.
  4. when releasing the dragging, the image need to animate back to the center of the screen, and scale to its origin size ( also animated )

I found many code samples and tried to implement it on my own but the combination of all the requirements made it very difficult and I couldn't get a decent result, so please don't put links to stuff from the first page of a Google search as I've spend many days trying to implement those examples, I would appreciate a working code example + xml layout (if needed)

like image 685
Orr Avatar asked Nov 14 '14 22:11

Orr


2 Answers

You can do this without much complication by combining:

  • A View.OnTouchListener (to detect the dragging sequence, basically ACTION_DOWN, ACTION_MOVE, ..., ACTION_UP).
  • Jake Wharton's excellent NineOldAndroids library library, to support view property animations before API level 11.

First, get the ImageView object and attach the View.OnTouchListener to it.

@Override
protected void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);

    ...
    mImage = findViewById(...);
    mImage.setOnTouchListener(mTouchListener);
}

Secondly, program the OnTouchListener to capture the ACTION_DOWN event and store the initial touch position X coordinate. Then for each ACTION_MOVE calculate the delta (for the translation and scaling) and for ACTION_UP return the ImageView to its initial state.

private View.OnTouchListener mTouchListener = new View.OnTouchListener()
{
    private float mStartX;

    @Override
    public boolean onTouch(View v, MotionEvent event)
    {
        switch (event.getActionMasked())
        {
            case MotionEvent.ACTION_DOWN :
                mStartX = event.getRawX();
                return true;

            case MotionEvent.ACTION_MOVE :
                float currentX = event.getRawX();
                animateTo(currentX - mStartX, true); // Snap to drag
                return true;

            case MotionEvent.ACTION_UP :
            case MotionEvent.ACTION_CANCEL :
                animateTo(0, false); // Ease back
                return true;
        }

        return false;
    }
};

The animateTo() would be implemented as follows. The immediate flag indicates whether the translation and scaling should be, well, immediate (it's true when responding to each move event, and false when easing back into its initial position and scale).

private void animateTo(float displacement, boolean immediate)
{
    final int EASE_BACK_DURATION = 300; // ms
    int duration = (immediate ? 0 : EASE_BACK_DURATION);

    Display display = getWindowManager().getDefaultDisplay();
    int totalWidth = display.getWidth();
    float scale = 1.0f - Math.abs(displacement / totalWidth);

    ViewPropertyAnimator.animate(mImage)
        .translationX(displacement)
        .scaleX(scale)
        .scaleY(scale)
        .setDuration(duration)
        .start();
}

You may want to tinker with the scaling. As written, it means that the image will be at 50% of its original size when dragged all the way to the border of the screen.

This solution should work without problems in API level 8 (though I haven't tested it). Full gist is available here if you want it.

like image 70
matiash Avatar answered Sep 27 '22 17:09

matiash


The first thing that came to mind was animating the LayoutParams, via a Handler. Not sure if it will fit your requirements and this probably requires some more testing.

In any case it was pretty fun remembering the math ^^ So here's my go at it, using only the native android tools:

Code:

package com.example.simon.draggableimageview;

import android.os.Handler;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;

/**
 * Created by Simon on 2014 Nov 18.
 */

public class MainActivity extends ActionBarActivity {

    private static final String TAG = "MainActivity";

    // Avoid small values for the following two or setSize will start lagging behind
    // The maximum time, animation (from smallest) to default size will take
    private static final int MAX_DURATION = 500;
    // Minimum delay (ms) for each loop
    private static final int MIN_DELAY = 20;

    // By how many px (at least) each (animation back to default state) loop will shift the image
    private static final float MIN_X_SHIFT = 3;

    private ImageView mImage;
    private int mInitialW, mInitialH, mCenterX;
    private int mMaxMargin;
    private AnimateBack mAnimateBack;
    private Handler mHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mHandler = new Handler();
        mImage = (ImageView) findViewById(R.id.imageView);
        final RelativeLayout imageHolder = (RelativeLayout) findViewById(R.id.imageHolder);

        mImage.post(new Runnable() {
            @Override
            public void run() {
                // Image ready, measure it
                mInitialH = mImage.getHeight();
                mInitialW = mImage.getWidth();

                imageHolder.post(new Runnable() {
                    @Override
                    public void run() {
                        // Calc other measurements
                        int containerWidth = imageHolder.getWidth();
                        mCenterX = containerWidth / 2;
                        mMaxMargin = containerWidth - mInitialW;
                    }
                });
            }
        });

        imageHolder.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                switch (motionEvent.getAction()) {
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:
                        mAnimateBack = new AnimateBack();
                        mAnimateBack.run();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        mHandler.removeCallbacks(mAnimateBack);
                        if (motionEvent.getX() > mMaxMargin + mInitialW || motionEvent.getX() < 0) {
                            // Fake Action_Up if out of container bounds
                            motionEvent.setAction(MotionEvent.ACTION_UP);
                            onTouch(view, motionEvent);
                            return true;
                        }
                        setSize(motionEvent.getX() - mCenterX);
                        break;
                }
                return true;
            }
        });
    }

    private void setSize(float offsetFromCenter) {
        // Calculate new left margin
        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) mImage.getLayoutParams();
        params.leftMargin = (int) (mMaxMargin * offsetFromCenter / (mCenterX - mInitialW / 2.0));

        // Calculate dimensions
        float ratio = 1 - (Math.abs(offsetFromCenter) / mCenterX);
        params.width = (int) (mInitialW * ratio);
        params.height = (int) (mInitialH * ratio);
        mImage.setLayoutParams(params);

//      Log.e(TAG, String.format("leftMargin: %d, W: %d, H: %d",
//              params.leftMargin, params.width, params.height));
    }

    private class AnimateBack implements Runnable {
        private int loopCount, loopDelay;
        private float loopBy;

        public AnimateBack() {
            RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) mImage.getLayoutParams();
            float offsetFromCenter = (float) params.leftMargin / mMaxMargin *
                    (mCenterX - mInitialW / 2.0f);
            float totalDuration = (Math.abs(offsetFromCenter) * MAX_DURATION / mCenterX);

            loopBy = MIN_X_SHIFT;
            loopCount = (int) Math.abs(offsetFromCenter / loopBy);
            loopDelay = (int) (totalDuration / loopCount);
            if (loopDelay < MIN_DELAY) {
                // Use the minimum delay
                loopDelay = MIN_DELAY;
                // Minimum loop count is 1
                loopCount = (int) Math.max(totalDuration / loopDelay, 1);
                // Calculate by how much each loop will inc/dec the margin
                loopBy = Math.round(Math.abs(offsetFromCenter / loopCount));
            }
            Log.d(TAG, String.format("Animate back will take: %fms. Will start from offset %d. " +
                            "It will advance by %dpx every %dms",
                    totalDuration, (int) offsetFromCenter, (int) loopBy, loopDelay));
        }

        @Override
        public void run() {
            --loopCount;
            RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) mImage.getLayoutParams();
            // Calculate offsetFromCenter
            float offsetFromCenter = (float) ((float) params.leftMargin / mMaxMargin *
                    (mCenterX - mInitialW / 2.0));
            // Don't pass 0 when looping
            if (params.leftMargin > 0) {
                offsetFromCenter = Math.max(offsetFromCenter - loopBy, 0);
            } else {
                offsetFromCenter = Math.min(offsetFromCenter + loopBy, 0);
            }
            setSize(offsetFromCenter);

            if (loopCount == 0) {
                mHandler.removeCallbacks(this);
            } else {
                mHandler.postDelayed(this, loopDelay);
            }
        }
    }

}

Layout:

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <RelativeLayout
        android:id="@+id/imageHolder"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center">

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:src="@drawable/ic_launcher"/>

    </RelativeLayout>
</RelativeLayout>

Preview:

enter image description here

like image 38
Simas Avatar answered Sep 27 '22 19:09

Simas