I need to implement an Animation on an ImageView, similar to the slide to answer animation that exists in many Android devices. The requiremnts:
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)
You can do this without much complication by combining:
View.OnTouchListener
(to detect the dragging sequence, basically ACTION_DOWN
, ACTION_MOVE
, ..., ACTION_UP
).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.
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:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With