The first thing that came to mind was the LayoutParams animation, through Handler . Not sure if it will meet your requirements and this will probably require a few more tests.
In any case, it was pretty funny to memorize the math ^^ So, here I am on it, using only my own tools for Android:
the 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); } } } }
Markup:
<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:

source share