Explorar el Código

1.修改RA Image Android图片列表删除后视图更新不正确。
2.修改RA Image Android图片预览,增加缩放功能。

Pen Li hace 9 años
padre
commit
f3e3677e40

+ 7 - 7
RA Image/app/app.iml

@@ -62,13 +62,6 @@
       <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
       <sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
       <sourceFolder url="file://$MODULE_DIR$/src/main/shaders" isTestSource="false" />
-      <sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" />
-      <sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
-      <sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" />
-      <sourceFolder url="file://$MODULE_DIR$/src/test/aidl" isTestSource="true" />
-      <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
-      <sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" />
-      <sourceFolder url="file://$MODULE_DIR$/src/test/shaders" isTestSource="true" />
       <sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
       <sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
       <sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
@@ -76,6 +69,13 @@
       <sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
       <sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
       <sourceFolder url="file://$MODULE_DIR$/src/androidTest/shaders" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/aidl" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/shaders" isTestSource="true" />
       <excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
       <excludeFolder url="file://$MODULE_DIR$/build/intermediates/blame" />
       <excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />

+ 86 - 16
RA Image/app/src/main/java/com/usai/redant/raimage/PhotoList/NewPhotoPreviewActivity.java

@@ -4,6 +4,8 @@ import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.net.Uri;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
 import android.support.v7.app.ActionBar;
 import android.support.v7.app.AppCompatActivity;
 import android.os.Bundle;
@@ -18,6 +20,8 @@ import android.widget.ImageView;
 import android.widget.TextView;
 
 import com.usai.redant.raimage.PhotoList.GridLayoutRecycler.RAPreviewRecyclerView;
+import com.usai.redant.raimage.PhotoList.XuanImageView.XuanImageView;
+import com.usai.redant.raimage.PhotoList.XuanImageView.XuanImageViewSettings;
 import com.usai.redant.raimage.R;
 
 import java.io.File;
@@ -26,8 +30,11 @@ import java.util.ArrayList;
 public class NewPhotoPreviewActivity extends AppCompatActivity {
 
     private TextView indexTV;
-    private RAPreviewRecyclerView recyclerView;
+//    private RAPreviewRecyclerView recyclerView;
+
+    private ViewPager pager;
     private ArrayList<String> photos;
+    private ArrayList<XuanImageView> ivContainer;
     private int currentIdx;
     private PreviewAdapter adapter;
     @Override
@@ -41,9 +48,11 @@ public class NewPhotoPreviewActivity extends AppCompatActivity {
         mActionBar.setDisplayHomeAsUpEnabled(true);
         mActionBar.setTitle("RA Image");
 
+
+        ivContainer = new ArrayList<XuanImageView>();
         /**View*/
         indexTV = (TextView)findViewById(R.id.index_tv);
-        recyclerView = (RAPreviewRecyclerView) findViewById(R.id.preview_recycler);
+//        recyclerView = (RAPreviewRecyclerView) findViewById(R.id.preview_recycler);
 
 
         /** Setting */
@@ -55,23 +64,81 @@ public class NewPhotoPreviewActivity extends AppCompatActivity {
             indexTV.setText(currentIdx + 1 + " / " + photos.size());
         }
 
-        LinearLayoutManager layoutManager = new LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false);
-        recyclerView.setLayoutManager(layoutManager);
-        // 设置Item增加、移除动画
-        recyclerView.setItemAnimator(new DefaultItemAnimator());
-        // 设置Adapter
-        recyclerView.setAdapter(adapter = new PreviewAdapter());
+//        LinearLayoutManager layoutManager = new LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false);
+//        recyclerView.setLayoutManager(layoutManager);
+//        // 设置Item增加、移除动画
+//        recyclerView.setItemAnimator(new DefaultItemAnimator());
+//        // 设置Adapter
+//        recyclerView.setAdapter(adapter = new PreviewAdapter());
+//
+//        recyclerView.indicator = new RAPreviewRecyclerView.PageIndicator() {
+//            @Override
+//            public void changePageIndex(int index, int total) {
+//                indexTV.setText(index + 1 + " / " + total);
+//            }
+//        };
+//
+//        recyclerView.scrollToPage(currentIdx);
+
+
+        pager = (ViewPager)findViewById(R.id.preview_recycler);
+        pager.setAdapter(new PagerAdapter() {
+            @Override
+            public int getCount() {
+                return photos.size();
+            }
+
+            @Override
+            public boolean isViewFromObject(View view, Object object) {
+                return view == object;
+            }
+
+            @Override
+            public Object instantiateItem(ViewGroup container, int position) {
+
+                XuanImageView xuanImageView = new XuanImageView(getBaseContext());
+                xuanImageView.setAutoRotateCategory(XuanImageViewSettings.AUTO_ROTATE_CATEGORY_MAGNETISM);
+
+                String path = photos.get(position);
+                Bitmap bitmap = scaleImage(path,150,150);
+                xuanImageView.setImageBitmap(bitmap);
+
+//                Bitmap bitmap = BitmapFactory.decodeFile(path);
+//                xuanImageView.setImageBitmap(bitmap);
+
+                container.addView(xuanImageView);
+
+                ivContainer.add(xuanImageView);
+
+                return  xuanImageView;
+
+            }
+
+            @Override
+            public void destroyItem(ViewGroup container, int position, Object object) {
+                ivContainer.remove(position);
+            }
+        });
+
+        pager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
+            @Override
+            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+
+            }
 
-        recyclerView.indicator = new RAPreviewRecyclerView.PageIndicator() {
             @Override
-            public void changePageIndex(int index, int total) {
-                indexTV.setText(index + 1 + " / " + total);
+            public void onPageSelected(int position) {
+                indexTV.setText(position + 1 + " / " + photos.size());
+                currentIdx = position;
             }
-        };
 
-        recyclerView.scrollToPage(currentIdx);
+            @Override
+            public void onPageScrollStateChanged(int state) {
 
+            }
+        });
 
+        pager.setCurrentItem(currentIdx);
 
     }
 
@@ -91,10 +158,10 @@ public class NewPhotoPreviewActivity extends AppCompatActivity {
     public class PreviewAdapter extends RecyclerView.Adapter<PreviewAdapter.PreviewHolder> {
 
         class PreviewHolder extends RecyclerView.ViewHolder {
-            ImageView imageView;
+            XuanImageView imageView;
             public PreviewHolder(View view) {
                 super(view);
-                imageView = (ImageView)view.findViewById(R.id.preview_item);
+                imageView = (XuanImageView)view.findViewById(R.id.preview_item);
             }
         }
 
@@ -122,10 +189,13 @@ public class NewPhotoPreviewActivity extends AppCompatActivity {
         }
     }
 
+
+    /** View Pager */
+
     public Bitmap scaleImage(String path, int width, int height) {
         Bitmap source = BitmapFactory.decodeFile(path);
 
-        Bitmap scaled = Bitmap.createScaledBitmap(source, width, height, true);
+        Bitmap scaled = Bitmap.createScaledBitmap(source, source.getWidth() / 2, source.getHeight() / 2, true);
         return scaled;
     }
 

+ 8 - 1
RA Image/app/src/main/java/com/usai/redant/raimage/PhotoList/PhotoListActivity.java

@@ -61,10 +61,11 @@ public class PhotoListActivity extends AppCompatActivity {
                         // 删除文件
                         new File(photos.get(i)).delete();
                         photos.remove(i);
-
+                        deletIndexs.set(i,"0");
                     }
                     adapter.notifyDataSetChanged();
                 }
+
             }
         });
 
@@ -147,6 +148,12 @@ public class PhotoListActivity extends AppCompatActivity {
 
             holder.photoView.setImageBitmap(bitmap);
 
+            holder.checkBtn.setBackgroundDrawable(getResources().getDrawable(R.drawable.check_none));
+            int delete = Integer.valueOf(deletIndexs.get(position));
+            if (delete == 1) {
+                holder.checkBtn.setBackgroundDrawable(getResources().getDrawable(R.drawable.check_check));
+            }
+
             holder.checkBtn.setOnClickListener(new View.OnClickListener() {
                 @Override
                 public void onClick(View v) {

+ 202 - 0
RA Image/app/src/main/java/com/usai/redant/raimage/PhotoList/XuanImageView/RotationGestureDetector.java

@@ -0,0 +1,202 @@
+package com.usai.redant.raimage.PhotoList.XuanImageView;
+
+import android.util.Log;
+import android.view.MotionEvent;
+
+/**
+ * Created by xuanyihuang on 9/4/16.
+ */
+
+public class RotationGestureDetector {
+    private int mXuanImageViewWith;
+    private static final int INVALID_POINTER_ID = -1;
+    private float sX, sY, fX, fY;
+    private float nfX, nfY, nsX, nsY;
+    private int ptrID1, ptrID2;
+    private int ptrID1_Index, ptrID2_Index, ptr_Index;
+    private float mAngle;
+    private float mPreviousAngle;
+    private float mAngleAtPresent;
+    private float mPreviousAngleAtPresent;
+    private float mPivotX;
+    private float mPivotY;
+    private boolean mIsRotated;
+    private int mPointerCount;
+    private float mBasicRotationTrigger;
+    private float mRotationTrigger;
+
+    private OnRotationGestureListener mListener;
+
+    public float getAngle() {
+        return mAngle;
+    }
+
+    public float getPreviousAngle(){
+        return  mPreviousAngle;
+    }
+
+    public float getPivotX(){
+        return mPivotX;
+    }
+
+    public float getPivotY(){
+        return mPivotY;
+    }
+
+    public void setAngle(float angle){
+        mAngle = angle;
+    }
+
+    public void setPreviousAngle(float angle){
+        mPreviousAngle = angle;
+    }
+
+    public boolean IsRotated(){
+        return  mIsRotated;
+    }
+
+    public RotationGestureDetector(OnRotationGestureListener listener, int XuanImageViewWidth){
+        mXuanImageViewWith = XuanImageViewWidth;
+        mListener = listener;
+        ptrID1 = INVALID_POINTER_ID;
+        ptrID2 = INVALID_POINTER_ID;
+        ptrID1_Index = INVALID_POINTER_ID;
+        ptrID2_Index = INVALID_POINTER_ID;
+        ptr_Index = INVALID_POINTER_ID;
+        mAngle = 0;
+        mPreviousAngle = 0;
+        mPivotX = 0;
+        mPivotY = 0;
+        mIsRotated = false;
+        mPointerCount = 0;
+        mBasicRotationTrigger = 10; //basic rotation trigger : 10 degrees
+        mRotationTrigger = mBasicRotationTrigger;
+    }
+
+    public boolean onTouchEvent(MotionEvent event){
+        Log.d("RotationGestureDetector", event + "");
+        switch (event.getAction() & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_DOWN:
+                //ptrID1 : first finger pressing down
+                ptrID1 = event.getPointerId(event.getActionIndex());
+                break;
+            case MotionEvent.ACTION_POINTER_DOWN:
+                //ptrID2: second finger pressing down
+                mPointerCount = event.getPointerCount();
+                if(mPointerCount == 2) {
+                    ptrID2 = event.getPointerId(event.getActionIndex());
+                    ptrID1_Index = event.findPointerIndex(ptrID1);
+                    ptrID2_Index = event.findPointerIndex(ptrID2);
+                    try {
+                        sX = event.getX(ptrID1_Index);
+                        sY = event.getY(ptrID1_Index);
+                        fX = event.getX(ptrID2_Index);
+                        fY = event.getY(ptrID2_Index);
+                        determineRotationTrigger();
+                    }catch(Exception e){
+                        //pointer index out of range exception
+                        return true;
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_MOVE:
+                Log.d("canStillRotate" , "" + canStillRotate());
+                if(canStillRotate()){
+                    ptrID1_Index = event.findPointerIndex(ptrID1);
+                    ptrID2_Index = event.findPointerIndex(ptrID2);
+                    try {
+                        nsX = event.getX(ptrID1_Index);
+                        nsY = event.getY(ptrID1_Index);
+                        nfX = event.getX(ptrID2_Index);
+                        nfY = event.getY(ptrID2_Index);
+                    } catch (Exception e){
+                        //pointer index out of range exception
+                        return true;
+                    }
+                    mPivotX = (nsX + nfX) / 2.0f;
+                    mPivotY = (nsY + nfY) / 2.0f;
+
+                    mAngleAtPresent = angleBetweenLines(fX, fY, sX, sY, nfX, nfY, nsX, nsY);
+
+                    if (mListener != null) {
+                        if(mIsRotated){
+                            mPreviousAngle = mAngle;
+                            mAngle = mPreviousAngle + (mAngleAtPresent - mPreviousAngleAtPresent);
+                            mPreviousAngleAtPresent = mAngleAtPresent;
+                            mListener.OnRotate(this);
+                        }
+                        else if(Math.abs(mAngleAtPresent) >= mRotationTrigger){
+                            sX = nsX;
+                            sY = nsY;
+                            fX = nfX;
+                            fY = nfY;
+                            mPreviousAngleAtPresent = 0;
+                            mIsRotated = true;
+                        }
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                invalidateTouchPointers();
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+                ptr_Index = event.getActionIndex();
+                if(ptrID1 == event.getPointerId(ptr_Index) || ptrID2 == event.getPointerId(ptr_Index)){
+                    invalidateTouchPointers();
+                    if(mListener != null){
+                        if(mIsRotated) {
+                            mListener.StopRotate(this);
+                            mIsRotated = false;
+                        }
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                invalidateTouchPointers();
+                break;
+        }
+
+        return true;
+    }
+
+    private boolean canStillRotate() {
+        return (ptrID1 != INVALID_POINTER_ID) && (ptrID2 != INVALID_POINTER_ID) ;
+    }
+
+    private void invalidateTouchPointers(){
+        ptrID1 = INVALID_POINTER_ID;
+        ptrID2 = INVALID_POINTER_ID;
+    }
+
+    private float angleBetweenLines (float fX, float fY, float sX, float sY, float nfX, float nfY, float nsX, float nsY)
+    {
+        float angle1 = (float) Math.atan2( (fY - sY), (fX - sX) );
+        float angle2 = (float) Math.atan2( (nfY - nsY), (nfX - nsX) );
+
+        //angle range: [-180 degrees, +180 degrees]
+        float angle = ((float)Math.toDegrees(angle2 - angle1)) % 360;
+        if (angle < -180.f) angle += 360.0f;
+        if (angle > 180.f) angle -= 360.0f;
+
+        Log.d("AngleBetweenLines :","" + angle);
+        return angle;
+    }
+
+    private double lineDistance(float fX, float fY, float sX, float sY){
+        return  Math.sqrt((fX - sX) * (fX - sX) + (fY - sY) * (fY - sY));
+    }
+
+    private void determineRotationTrigger(){
+        // need further optimization
+        if(lineDistance(fX,fY,sX,sY) <= (mXuanImageViewWith / 3)){
+            mRotationTrigger = mBasicRotationTrigger * 2;
+        }
+        else
+            mRotationTrigger = mBasicRotationTrigger;
+    }
+
+    public interface OnRotationGestureListener {
+        boolean OnRotate(RotationGestureDetector rotationGestureDetector);
+        boolean StopRotate(RotationGestureDetector rotationGestureDetector);
+    }
+}

+ 1033 - 0
RA Image/app/src/main/java/com/usai/redant/raimage/PhotoList/XuanImageView/XuanImageView.java

@@ -0,0 +1,1033 @@
+package com.usai.redant.raimage.PhotoList.XuanImageView;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+
+import com.usai.redant.raimage.R;
+
+/**
+ * Created by xuanyihuang on 8/30/16.
+ */
+
+public class XuanImageView extends android.support.v7.widget.AppCompatImageView{
+    private int XuanImageViewWidth;
+    private int XuanImageViewHeight;
+    private int XuanImageViewCenterX;
+    private int XuanImageViewCenterY;
+    private int ImageCenterX;
+    private int ImageCenterY;
+    private int mOrientation;   //1 for portrait, 2 for landscape
+    private int mAutoRotateCategory;
+    private float mInitScale;   //for landscape
+    private float mInitPortraitScale;
+    private float mTempInitPortraitScale;
+    private float mMaxScale;
+    private float mPortraitMaxScale;
+    private float mTempPortraitMaxScale;
+    private boolean mRotationToggle;
+    private float mMaxScaleMultiple;
+    private float mDoubleTabScaleMultiple;
+    private float mDoubleTabScale;
+    private float mPortraitDoubleTabScale;
+    private float mTempPortraitDoubleTabScale;
+    private Matrix mScaleMatrix;
+    private ScaleGestureDetector mScaleGestureDetector;
+    private GestureDetector mGestureDetector;
+    private float mLastScaleFocusX;
+    private float mLastScaleFocusY;
+    private boolean isAutoScale;
+    private float mSpringBackGradientScaleUpLevel;
+    private float mSpringBackGradientScaleDownLevel;
+    private float mDoubleTapGradientScaleUpLevel;
+    private float mDoubleTapGradientScaleDownLevel;
+    private int mLastPointerCount;
+    private float mLastX;
+    private float mLastY;
+    private float mAngle;
+    private float mPreviousAngle;
+    private RotationGestureDetector mRotateGestureDetector;
+    private boolean isAutoRotated;
+    private double allowableFloatError;
+    private double allowablePortraitFloatError;
+    private float currentScaleLevel;
+    private float currentAbsScaleLevel;
+    private float autoRotationTrigger;
+    private int autoRotationRunnableDelay;
+    private int autoRotationRunnableTimes;
+    private int doubleTabScaleRunnableDelay;
+    private int springBackRunnableDelay;
+    private boolean hasDrawable;
+    private boolean knowViewSize;
+
+    public XuanImageView(Context context) {
+        this(context, null);
+    }
+
+    public XuanImageView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public XuanImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        initialize(context, attrs);
+    }
+
+    private void initialize(Context context, AttributeSet attrs) {
+        setScaleType(ScaleType.MATRIX);
+        mScaleMatrix = new Matrix();
+
+        mScaleGestureDetector = new ScaleGestureDetector(context, constructOnScaleGestureListener());
+        mGestureDetector = new GestureDetector(context, constructOnGestureListener());
+
+        initCustomAttrs(context, attrs);
+
+        mOrientation = XuanImageViewSettings.ORIENTATION_LANDSCAPE;   //1 for portrait, 2 for landscape
+        isAutoScale = false;
+        mLastPointerCount = 0;
+        mAngle = 0;
+        mPreviousAngle = 0;
+        currentScaleLevel = 1;
+    }
+
+    private ScaleGestureDetector.OnScaleGestureListener constructOnScaleGestureListener() {
+        return new ScaleGestureDetector.OnScaleGestureListener() {
+            @Override
+            public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
+                float scaleFactor = scaleGestureDetector.getScaleFactor();
+
+                currentScaleLevel = getCurrentScaleLevel();
+                currentAbsScaleLevel = Math.abs(currentScaleLevel);
+                Log.d("CurrentAbsScaleLevel", "" + currentAbsScaleLevel);
+
+                boolean isRotating = false;
+                boolean justScale = false;
+
+                if(mRotateGestureDetector.IsRotated()){
+                    // is rotating
+                    isRotating = true;
+                }
+                else{
+                    // not rotating, just scaling
+                    if(mAutoRotateCategory == XuanImageViewSettings.AUTO_ROTATE_CATEGORY_RESTORATION){
+                        if((currentScaleLevel <= mMaxScale && scaleFactor > 1.0f) || (currentScaleLevel >= mInitScale && scaleFactor < 1.0f))
+                            justScale = true;
+
+                    }
+                    else if(mAutoRotateCategory == XuanImageViewSettings.AUTO_ROTATE_CATEGORY_MAGNETISM){
+                        if (mOrientation == XuanImageViewSettings.ORIENTATION_LANDSCAPE) {
+                            if ((currentAbsScaleLevel <= mMaxScale && scaleFactor > 1.0f) || (currentAbsScaleLevel >= mInitScale && scaleFactor < 1.0f))
+                                justScale = true;
+                        } else if (mOrientation == XuanImageViewSettings.ORIENTATION_PORTRAIT) {
+                            if ((currentAbsScaleLevel <= mTempPortraitMaxScale && scaleFactor > 1.0f) || (currentAbsScaleLevel >= mTempInitPortraitScale && scaleFactor < 1.0f))
+                                justScale = true;
+                        }
+                    }
+                }
+
+
+                if(isRotating) {
+                    mScaleMatrix.postScale(scaleFactor, scaleFactor, mRotateGestureDetector.getPivotX(), mRotateGestureDetector.getPivotY());
+                }
+                else if(justScale)
+                {
+                    mScaleMatrix.postScale(scaleFactor, scaleFactor, scaleGestureDetector.getFocusX(), scaleGestureDetector.getFocusY());
+                    checkBorderAndCenterWhenScale();
+                }
+
+
+                setImageMatrix(mScaleMatrix);
+                mLastScaleFocusX = scaleGestureDetector.getFocusX();
+                mLastScaleFocusY = scaleGestureDetector.getFocusY();
+
+                return true;
+            }
+
+            @Override
+            public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
+                Log.d("onScaleBegin-->", "");
+                return true;
+            }
+
+            @Override
+            public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) {
+                Log.d("onScaleEnd-->", "");
+            }
+        };
+    }
+
+    private GestureDetector.OnGestureListener constructOnGestureListener() {
+        return new GestureDetector.SimpleOnGestureListener() {
+            @Override
+            public boolean onDoubleTap(MotionEvent e) {
+                currentScaleLevel = getCurrentScaleLevel();
+                currentAbsScaleLevel = Math.abs(currentScaleLevel);
+                float x = e.getX();
+                float y = e.getY();
+
+                if (isAutoScale)
+                    return true;
+
+
+                if (mAutoRotateCategory == XuanImageViewSettings.AUTO_ROTATE_CATEGORY_RESTORATION) {
+                    if (currentScaleLevel < mDoubleTabScale) {
+                        postDelayed(new AutoScaleRunnable(mDoubleTabScale, x, y, mDoubleTapGradientScaleUpLevel, mDoubleTapGradientScaleDownLevel), doubleTabScaleRunnableDelay);
+                        isAutoScale = true;
+                    } else {
+                        postDelayed(new AutoScaleRunnable(mInitScale, x, y, mDoubleTapGradientScaleUpLevel, mDoubleTapGradientScaleDownLevel), doubleTabScaleRunnableDelay);
+                        isAutoScale = true;
+                    }
+                } else if (mAutoRotateCategory == XuanImageViewSettings.AUTO_ROTATE_CATEGORY_MAGNETISM) {
+                    if (mOrientation == XuanImageViewSettings.ORIENTATION_LANDSCAPE) {
+                        if (currentAbsScaleLevel < mDoubleTabScale) {
+                            postDelayed(new AutoScaleRunnable(mDoubleTabScale * (currentScaleLevel / currentAbsScaleLevel), x, y, mDoubleTapGradientScaleUpLevel, mDoubleTapGradientScaleDownLevel), doubleTabScaleRunnableDelay);
+                            isAutoScale = true;
+                        } else {
+                            postDelayed(new AutoScaleRunnable(mInitScale * (currentScaleLevel / currentAbsScaleLevel), x, y, mDoubleTapGradientScaleUpLevel, mDoubleTapGradientScaleDownLevel), doubleTabScaleRunnableDelay);
+                            isAutoScale = true;
+                        }
+                    } else if (mOrientation == XuanImageViewSettings.ORIENTATION_PORTRAIT) {
+                        if (currentAbsScaleLevel < mTempPortraitDoubleTabScale) {
+                            postDelayed(new AutoScaleRunnable(mTempPortraitDoubleTabScale * (currentScaleLevel / currentAbsScaleLevel), x, y, mDoubleTapGradientScaleUpLevel, mDoubleTapGradientScaleDownLevel), doubleTabScaleRunnableDelay);
+                            isAutoScale = true;
+                        } else {
+                            postDelayed(new AutoScaleRunnable(mTempInitPortraitScale * (currentScaleLevel / currentAbsScaleLevel), x, y, mDoubleTapGradientScaleUpLevel, mDoubleTapGradientScaleDownLevel), doubleTabScaleRunnableDelay);
+                            isAutoScale = true;
+                        }
+                    }
+                }
+
+                return true;
+            }
+        };
+    }
+
+
+    private RotationGestureDetector.OnRotationGestureListener constructOnRotationGestureListener(){
+        return new RotationGestureDetector.OnRotationGestureListener() {
+            @Override
+            public boolean OnRotate(RotationGestureDetector rotationGestureDetector) {
+                mAngle = rotationGestureDetector.getAngle();
+                mPreviousAngle = rotationGestureDetector.getPreviousAngle();
+                mScaleMatrix.postRotate(mAngle - mPreviousAngle, rotationGestureDetector.getPivotX(), rotationGestureDetector.getPivotY());
+
+                setImageMatrix(mScaleMatrix);
+
+                return true;
+            }
+
+            @Override
+            public boolean StopRotate(RotationGestureDetector rotationGestureDetector) {
+                if (mAutoRotateCategory == XuanImageViewSettings.AUTO_ROTATE_CATEGORY_RESTORATION)
+                    AutoRotateRestoration(rotationGestureDetector);
+                else if (mAutoRotateCategory == XuanImageViewSettings.AUTO_ROTATE_CATEGORY_MAGNETISM)
+                    AutoRotateMagnetism(rotationGestureDetector);
+
+                return true;
+            }
+        };
+    }
+
+    private void initCustomAttrs(Context context, AttributeSet attrs){
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.xuanimageview);
+        mRotationToggle = a.getBoolean(R.styleable.xuanimageview_RotationToggle, true);
+        mAutoRotateCategory = a.getInteger(R.styleable.xuanimageview_AutoRotateCategory, XuanImageViewSettings.AUTO_ROTATE_CATEGORY_RESTORATION);
+        mMaxScaleMultiple = a.getFloat(R.styleable.xuanimageview_MaxScaleMultiple, 4);
+        mDoubleTabScaleMultiple = a.getFloat(R.styleable.xuanimageview_DoubleTabScaleMultiple, 2);
+        mSpringBackGradientScaleUpLevel = a.getFloat(R.styleable.xuanimageview_SpringBackGradientScaleUpLevel, 1.01f);
+        mSpringBackGradientScaleDownLevel = a.getFloat(R.styleable.xuanimageview_SpringBackGradientScaleDownLevel, 0.99f);
+        mDoubleTapGradientScaleUpLevel = a.getFloat(R.styleable.xuanimageview_DoubleTapGradientScaleUpLevel, 1.05f);
+        mDoubleTapGradientScaleDownLevel = a.getFloat(R.styleable.xuanimageview_DoubleTapGradientScaleDownLevel, 0.95f);
+        autoRotationTrigger = a.getFloat(R.styleable.xuanimageview_AutoRotationTrigger, 60);
+        springBackRunnableDelay = a.getInteger(R.styleable.xuanimageview_SpringBackRunnableDelay, 10);
+        doubleTabScaleRunnableDelay = a.getInteger(R.styleable.xuanimageview_DoubleTapScaleRunnableDelay, 10);
+        autoRotationRunnableDelay = a.getInteger(R.styleable.xuanimageview_AutoRotationRunnableDelay, 5);
+        autoRotationRunnableTimes = a.getInteger(R.styleable.xuanimageview_AutoRotationRunnableTimes, 10);
+        try {
+            allowableFloatError = Double.parseDouble(a.getString(R.styleable.xuanimageview_AllowableFloatError));
+        }catch (Exception e){
+            allowableFloatError = 1E-6;   // for normal display aspect ratio
+//        allowableFloatError = 3E-3;   // for Galaxy S8
+        }
+        try {
+            allowablePortraitFloatError = Double.parseDouble(a.getString(R.styleable.xuanimageview_AllowablePortraitFloatError));
+        }catch (Exception e){
+            allowablePortraitFloatError = 1E-12;  // for normal display aspect ratio
+//        allowablePortraitFloatError = 5E-8;   //for Galaxy S8
+        }
+        a.recycle();
+    }
+
+    @Override
+    public void setImageDrawable(Drawable drawable) {
+        super.setImageDrawable(drawable);
+
+        if (drawable == null) {
+            hasDrawable = false;
+            return;
+        }
+
+        if (!drawableHasSize(drawable))
+            return;
+
+        hasDrawable = true;
+        initDrawableMatrix();
+    }
+
+    @Override
+    public void setImageResource(int resId) {
+        Drawable drawable = null;
+        try {
+            drawable = getResources().getDrawable(resId);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        setImageDrawable(drawable);
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+
+        knowViewSize = true;
+        initDrawableMatrix();
+    }
+
+    private boolean drawableHasSize(Drawable drawable) {
+        if ((drawable.getIntrinsicHeight() <= 0 || drawable.getIntrinsicWidth() <= 0)
+                && (drawable.getMinimumHeight() <= 0 || drawable.getMinimumWidth() <= 0)
+                && (drawable.getBounds().height() <= 0 || drawable.getBounds().width() <= 0))
+            return false;
+        else
+            return true;
+    }
+
+    private void initDrawableMatrix() {
+        if (!hasDrawable)
+            return;
+        if (!knowViewSize)
+            return;
+
+        if (mScaleMatrix == null)
+            mScaleMatrix = new Matrix();
+        else
+            mScaleMatrix.reset();
+
+        // get width and height of XuanImageView
+        XuanImageViewWidth = getWidth();
+        XuanImageViewHeight = getHeight();
+
+        //get the center point of XuanImageView
+        XuanImageViewCenterX = XuanImageViewWidth / 2;
+        XuanImageViewCenterY = XuanImageViewHeight / 2;
+
+        // instantiate mRotationGestureDetector after dimension of XuanImageView is got.
+        mRotateGestureDetector = new RotationGestureDetector(constructOnRotationGestureListener(), XuanImageViewWidth);
+
+        //get width and height of the image
+        Drawable imageDrawable = getDrawable();
+        if (imageDrawable == null)
+            return;
+        int imageWidth = imageDrawable.getIntrinsicWidth();
+        int imageHeight = imageDrawable.getIntrinsicHeight();
+
+        //image is scaled to fit the size of XuanImageView at the very beginning.
+        float scale = Math.min(XuanImageViewWidth * 1.0f / imageWidth, XuanImageViewHeight * 1.0f / imageHeight);
+        float portraitscale = Math.min(XuanImageViewWidth * 1.0f / imageHeight, XuanImageViewHeight * 1.0f / imageWidth);
+
+        mInitScale = scale;
+        mInitPortraitScale = portraitscale;
+        mMaxScale = mMaxScaleMultiple * scale;
+        mPortraitMaxScale = mMaxScaleMultiple * portraitscale;
+        mDoubleTabScale = mDoubleTabScaleMultiple * scale;
+        mPortraitDoubleTabScale = mDoubleTabScaleMultiple * portraitscale;
+
+        //center of image overlaps with that of XuanImageView
+        int deltaX = XuanImageViewWidth / 2 - imageWidth / 2;
+        int deltaY = XuanImageViewHeight / 2 - imageHeight / 2;
+
+        mScaleMatrix.postTranslate(deltaX, deltaY);
+        mScaleMatrix.postScale(scale, scale, XuanImageViewWidth / 2, XuanImageViewHeight / 2);
+        setImageMatrix(mScaleMatrix);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent motionEvent) {
+        if (mRotateGestureDetector == null)
+            return true;
+
+        boolean parentDisallowInterceptTouchEventFlag = true;
+
+        // for DoubleTap gesture
+        mGestureDetector.onTouchEvent(motionEvent);
+
+        // for Scale gesture
+        mScaleGestureDetector.onTouchEvent(motionEvent);
+
+
+        currentScaleLevel = getCurrentScaleLevel();
+        currentAbsScaleLevel = Math.abs(currentScaleLevel);
+        if (mRotationToggle) {
+            if (mAutoRotateCategory == XuanImageViewSettings.AUTO_ROTATE_CATEGORY_RESTORATION) {
+                if (mRotateGestureDetector.IsRotated() || Math.abs(currentScaleLevel - mInitScale) < allowableFloatError
+                        || ((motionEvent.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP)
+                        || ((motionEvent.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP)) {
+                    if (!mRotateGestureDetector.IsRotated())
+                        parentDisallowInterceptTouchEventFlag = false;
+
+                    // for Rotation gesture
+                    mRotateGestureDetector.onTouchEvent(motionEvent);
+                }
+            } else if (mAutoRotateCategory == XuanImageViewSettings.AUTO_ROTATE_CATEGORY_MAGNETISM) {
+                if (mOrientation == XuanImageViewSettings.ORIENTATION_LANDSCAPE) {
+                    if (mRotateGestureDetector.IsRotated() || Math.abs(currentAbsScaleLevel - mInitScale) < allowableFloatError
+                            || ((motionEvent.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP)
+                            || ((motionEvent.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP)) {
+                        if (!mRotateGestureDetector.IsRotated())
+                            parentDisallowInterceptTouchEventFlag = false;
+
+                        // for Rotation gesture
+                        mRotateGestureDetector.onTouchEvent(motionEvent);
+
+                    }
+                } else if (mOrientation == XuanImageViewSettings.ORIENTATION_PORTRAIT) {
+                    if (mRotateGestureDetector.IsRotated() || Math.abs(currentAbsScaleLevel - mTempInitPortraitScale) < allowablePortraitFloatError
+                            || ((motionEvent.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP)
+                            || ((motionEvent.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP)) {
+                        if (!mRotateGestureDetector.IsRotated())
+                            parentDisallowInterceptTouchEventFlag = false;
+
+                        // for Rotation gesture
+                        mRotateGestureDetector.onTouchEvent(motionEvent);
+                    }
+                }
+            }
+        }
+
+        //pointerCount won't be 0
+        int pointerCount = motionEvent.getPointerCount();
+        float pivotX = 0;
+        float pivotY = 0;
+        for (int i = 0; i < pointerCount; i++) {
+            pivotX += motionEvent.getX(i);
+            pivotY += motionEvent.getY(i);
+        }
+        pivotX /= pointerCount;   //get integer result of the division
+        pivotY /= pointerCount;
+
+        // when image is being dragged, generally, pointCount == mLastCount holds, so old state is not saved.
+        if (pointerCount != mLastPointerCount) {
+            mLastX = pivotX;
+            mLastY = pivotY;
+            mLastPointerCount = pointerCount;
+        }
+
+        RectF rectF = getMatrixRectF();
+
+        switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_DOWN:
+                if (getParent() != null)
+                    getParent().requestDisallowInterceptTouchEvent(parentDisallowInterceptTouchEventFlag);
+                break;
+            case MotionEvent.ACTION_MOVE:
+                if (getParent() != null)
+                    getParent().requestDisallowInterceptTouchEvent(parentDisallowInterceptTouchEventFlag);
+                float deltaX = pivotX - mLastX;
+                float deltaY = pivotY - mLastY;
+
+                if (getDrawable() != null) {
+                    if (!mRotateGestureDetector.IsRotated()) {
+                        if (rectF.width() <= XuanImageViewWidth)
+                            deltaX = 0;
+                        if (rectF.height() <= XuanImageViewHeight)
+                            deltaY = 0;
+                    }
+                    mScaleMatrix.postTranslate(deltaX, deltaY);
+
+                    if (!mRotateGestureDetector.IsRotated())
+                        checkBorderAndCenterWhenTranslate();
+                    setImageMatrix(mScaleMatrix);
+                }
+                mLastX = pivotX;
+                mLastY = pivotY;
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+                //one finger loosening (multi-touch)
+                if (pointerCount - 1 < 2) {
+                    if (mAutoRotateCategory == XuanImageViewSettings.AUTO_ROTATE_CATEGORY_RESTORATION) {
+                        //spring back to mInitScale or mMaxScale
+                        if ((currentScaleLevel < mInitScale) && !isAutoRotated) {
+                            postDelayed(new AutoScaleRunnable(mInitScale, mLastScaleFocusX, mLastScaleFocusY, mSpringBackGradientScaleUpLevel, mSpringBackGradientScaleDownLevel), springBackRunnableDelay);
+                            isAutoScale = true;
+                        } else if ((currentScaleLevel > mMaxScale) && !isAutoRotated) {
+                            postDelayed(new AutoScaleRunnable(mMaxScale, mLastScaleFocusX, mLastScaleFocusY, mSpringBackGradientScaleUpLevel, mSpringBackGradientScaleDownLevel), springBackRunnableDelay);
+                            isAutoScale = true;
+                        }
+                    } else if (mAutoRotateCategory == XuanImageViewSettings.AUTO_ROTATE_CATEGORY_MAGNETISM) {
+                        if (mOrientation == XuanImageViewSettings.ORIENTATION_LANDSCAPE) {
+                            if (currentAbsScaleLevel < mInitScale && !isAutoRotated) {
+                                postDelayed(new AutoScaleRunnable(mInitScale * (currentScaleLevel / currentAbsScaleLevel), mLastScaleFocusX, mLastScaleFocusY, mSpringBackGradientScaleUpLevel, mSpringBackGradientScaleDownLevel), springBackRunnableDelay);
+                                isAutoScale = true;
+                            } else if ((currentAbsScaleLevel > mMaxScale) && !isAutoRotated) {
+                                postDelayed(new AutoScaleRunnable(mMaxScale * (currentScaleLevel / currentAbsScaleLevel), mLastScaleFocusX, mLastScaleFocusY, mSpringBackGradientScaleUpLevel, mSpringBackGradientScaleDownLevel), springBackRunnableDelay);
+                                isAutoScale = true;
+                            }
+                        } else if (mOrientation == XuanImageViewSettings.ORIENTATION_PORTRAIT) {
+                            if (currentAbsScaleLevel < mTempInitPortraitScale && !isAutoRotated) {
+                                postDelayed(new AutoScaleRunnable(mTempInitPortraitScale * (currentScaleLevel / currentAbsScaleLevel), mLastScaleFocusX, mLastScaleFocusY, mSpringBackGradientScaleUpLevel, mSpringBackGradientScaleDownLevel), springBackRunnableDelay);
+                                isAutoScale = true;
+                            } else if ((currentAbsScaleLevel > mTempPortraitMaxScale) && !isAutoRotated) {
+                                postDelayed(new AutoScaleRunnable(mTempPortraitMaxScale * (currentScaleLevel / currentAbsScaleLevel), mLastScaleFocusX, mLastScaleFocusY, mSpringBackGradientScaleUpLevel, mSpringBackGradientScaleDownLevel), springBackRunnableDelay);
+                                isAutoScale = true;
+                            }
+                        }
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                mLastPointerCount = 0;
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                mLastPointerCount = 0;
+        }
+
+
+        return true;
+    }
+
+    private void checkBorderAndCenterWhenScale() {
+
+        RectF rectF = getMatrixRectF();
+        float deltaX = 0;
+        float deltaY = 0;
+
+        /**
+         * If width or height of image is bigger than that of XuanImageView,
+         * should prevent image's edge being far away from XuanImageView's edge.
+         */
+        if (rectF.width() >= XuanImageViewWidth) {
+            if (rectF.left > 0)
+                deltaX = -rectF.left;
+            if (rectF.right < XuanImageViewWidth)
+                deltaX = XuanImageViewWidth - rectF.right;
+
+        }
+        if (rectF.height() >= XuanImageViewHeight) {
+            if (rectF.top > 0)
+                deltaY = -rectF.top;
+            if (rectF.bottom < XuanImageViewHeight)
+                deltaY = XuanImageViewHeight - rectF.bottom;
+        }
+
+        /**
+         * If width or height of image is smaller than that of XuanImageView,
+         * make sure the image, in width dimension or height dimension, is centered.
+         */
+        if (rectF.width() < XuanImageViewWidth) {
+            deltaX = XuanImageViewWidth / 2.0f - rectF.left - rectF.width() / 2.0f;
+        }
+        if (rectF.height() < XuanImageViewHeight) {
+            deltaY = XuanImageViewHeight / 2.0f - rectF.top - rectF.height() / 2.0f;
+
+        }
+
+        mScaleMatrix.postTranslate(deltaX, deltaY);
+    }
+
+    private void checkBorderAndCenterWhenTranslate() {
+        /**
+         *  No need to check Center here because it has been handled before checkBorderAndCenterWhenTranslate() is invoked.
+         *  See "case MotionEvent.ACTION_MOVE: " in  onTouch(View view, MotionEvent motionEvent).
+         */
+        RectF rectF = getMatrixRectF();
+
+        float deltaX = 0;
+        float deltaY = 0;
+
+        /**
+         * If width or height of image is bigger than that of XuanImageView,
+         * should prevent image's edge being far away from XuanImageView's edge.
+         */
+        if (rectF.width() >= XuanImageViewWidth) {
+            if (rectF.left > 0)
+                deltaX = -rectF.left;
+            if (rectF.right < XuanImageViewWidth)
+                deltaX = XuanImageViewWidth - rectF.right;
+
+        }
+        if (rectF.height() >= XuanImageViewHeight) {
+            if (rectF.top > 0)
+                deltaY = -rectF.top;
+            if (rectF.bottom < XuanImageViewHeight)
+                deltaY = XuanImageViewHeight - rectF.bottom;
+        }
+
+        mScaleMatrix.postTranslate(deltaX, deltaY);
+    }
+
+
+    private float getCurrentScaleLevel() {
+        float matrixArray[] = new float[9];
+        mScaleMatrix.getValues(matrixArray);
+
+        return matrixArray[Matrix.MSCALE_X];
+    }
+
+    private RectF getMatrixRectF() {
+        Matrix matrix = mScaleMatrix;
+        RectF rectF = new RectF();
+        Drawable image = getDrawable();
+        if (image != null) {
+            rectF.set(0, 0, image.getIntrinsicWidth(), image.getIntrinsicHeight());
+            matrix.mapRect(rectF);
+        }
+
+        return rectF;
+    }
+
+    private boolean calculateImageCenterCoordinates() {
+        RectF rectF = getMatrixRectF();
+        ImageCenterX = (int) ((rectF.left + rectF.right) / 2);
+        ImageCenterY = (int) ((rectF.top + rectF.bottom) / 2);
+
+        return true;
+    }
+
+    public void AutoRotateMagnetism(RotationGestureDetector rotationGestureDetector) {
+        mAngle = rotationGestureDetector.getAngle();//mAngle's range: [-180 degress, 180 degress]
+        float autoRotateAngle;
+        int quotient;
+        float remainder;
+
+        quotient = ((int) mAngle) / 90;
+        remainder = mAngle % 90;
+
+        if (remainder >= autoRotationTrigger) {
+            autoRotateAngle = 90 - remainder;
+            if ((quotient + 1) % 2 == 0)
+                mOrientation = XuanImageViewSettings.ORIENTATION_LANDSCAPE;
+            else
+                mOrientation = XuanImageViewSettings.ORIENTATION_PORTRAIT;
+        } else if (remainder <= -autoRotationTrigger) {
+            autoRotateAngle = -90 - remainder;
+            if ((quotient - 1) % 2 == 0)
+                mOrientation = XuanImageViewSettings.ORIENTATION_LANDSCAPE;
+            else
+                mOrientation = XuanImageViewSettings.ORIENTATION_PORTRAIT;
+        } else {
+            autoRotateAngle = -remainder;
+            if (quotient % 2 == 0)
+                mOrientation = XuanImageViewSettings.ORIENTATION_LANDSCAPE;
+            else
+                mOrientation = XuanImageViewSettings.ORIENTATION_PORTRAIT;
+        }
+
+        postDelayed(new AutoRotateRunnable(autoRotateAngle, getCurrentScaleLevel() / (float) Math.cos(Math.toRadians(mAngle)), autoRotationRunnableTimes), autoRotationRunnableDelay);
+        isAutoRotated = true;
+
+        rotationGestureDetector.setAngle(mAngle + autoRotateAngle);
+        rotationGestureDetector.setPreviousAngle(mAngle + autoRotateAngle);
+    }
+
+    public void AutoRotateRestoration(RotationGestureDetector rotationGestureDetector) {
+        mAngle = rotationGestureDetector.getAngle();//mAngle's range: [-180 degress, 180 degress]
+        float autoRotateAngle;
+
+        if (mAngle >= autoRotationTrigger)
+            autoRotateAngle = 360 - mAngle;
+        else if (mAngle <= -autoRotationTrigger)
+            autoRotateAngle = -360 - mAngle;
+        else
+            autoRotateAngle = -mAngle;
+
+        postDelayed(new AutoRotateRunnable(autoRotateAngle, getCurrentScaleLevel() / (float) Math.cos(Math.toRadians(mAngle)), autoRotationRunnableTimes), autoRotationRunnableDelay);
+        isAutoRotated = true;
+
+        rotationGestureDetector.setAngle(0.0f);
+        rotationGestureDetector.setPreviousAngle(0.0f);
+    }
+
+    private class AutoScaleRunnable implements Runnable {
+        float targetScale;
+        float targetAbsScale;
+        float FocusX;
+        float FocusY;
+        float scaleFactor;
+
+        AutoScaleRunnable(float targetScale, float FocusX, float FocusY, float GradientScaleUp, float GradientScaleDown) {
+            this.targetScale = targetScale;
+            this.targetAbsScale = Math.abs(targetScale);
+            this.FocusX = FocusX;
+            this.FocusY = FocusY;
+            currentScaleLevel = getCurrentScaleLevel();
+            currentAbsScaleLevel = Math.abs(currentScaleLevel);
+            if (currentAbsScaleLevel < targetAbsScale)
+                scaleFactor = GradientScaleUp;
+            else if (currentAbsScaleLevel > targetAbsScale)
+                scaleFactor = GradientScaleDown;
+            else if (currentAbsScaleLevel == targetAbsScale)
+                scaleFactor = 1.0f;
+        }
+
+        @Override
+        public void run() {
+            mScaleMatrix.postScale(scaleFactor, scaleFactor, FocusX, FocusY);
+            checkBorderAndCenterWhenScale();
+            setImageMatrix(mScaleMatrix);
+
+            currentScaleLevel = getCurrentScaleLevel();
+            currentAbsScaleLevel = Math.abs(currentScaleLevel);
+            if ((scaleFactor < 1.0f && currentAbsScaleLevel > targetAbsScale)
+                    || (scaleFactor > 1.0f && currentAbsScaleLevel < targetAbsScale)) {
+                postDelayed(this, springBackRunnableDelay);
+            } else {
+                scaleFactor = targetScale / currentScaleLevel;
+
+                mScaleMatrix.postScale(scaleFactor, scaleFactor, FocusX, FocusY);
+                checkBorderAndCenterWhenScale();
+                setImageMatrix(mScaleMatrix);
+
+                isAutoScale = false;
+            }
+
+        }
+    }
+
+
+    private class AutoRotateRunnable implements Runnable {
+        float targetRotateAngle;
+        long TotalRotateTimes;
+        float AnglePerTime;
+        float AccumulativeRotateTimes;
+        float AccumulativeRotateAngles;
+        float initScaleLevel;
+        double ScalePerTime;
+
+        AutoRotateRunnable(float targetRotateAngle, float initScaleLevel, long TotalRotateTimes) {
+            this.targetRotateAngle = targetRotateAngle;
+            this.TotalRotateTimes = TotalRotateTimes;
+            AnglePerTime = targetRotateAngle / this.TotalRotateTimes;
+            AccumulativeRotateTimes = 0;
+            AccumulativeRotateAngles = 0.0f;
+            this.initScaleLevel = initScaleLevel;
+            if (mAutoRotateCategory == XuanImageViewSettings.AUTO_ROTATE_CATEGORY_RESTORATION) {
+                ScalePerTime = Math.pow(mInitScale / initScaleLevel, 1.0 / TotalRotateTimes);
+            } else if (mAutoRotateCategory == XuanImageViewSettings.AUTO_ROTATE_CATEGORY_MAGNETISM) {
+                if (mOrientation == XuanImageViewSettings.ORIENTATION_LANDSCAPE)
+                    ScalePerTime = Math.pow(mInitScale / initScaleLevel, 1.0 / TotalRotateTimes);
+                else if (mOrientation == XuanImageViewSettings.ORIENTATION_PORTRAIT)
+                    ScalePerTime = Math.pow(mInitPortraitScale / initScaleLevel, 1.0 / TotalRotateTimes);
+            }
+
+        }
+
+        @Override
+        public void run() {
+            calculateImageCenterCoordinates();
+
+            mScaleMatrix.postRotate(AnglePerTime, ImageCenterX, ImageCenterY);
+            mScaleMatrix.postScale((float) ScalePerTime, (float) ScalePerTime, ImageCenterX, ImageCenterY);
+            mScaleMatrix.postTranslate((XuanImageViewCenterX - ImageCenterX) / (TotalRotateTimes - AccumulativeRotateTimes), (XuanImageViewCenterY - ImageCenterY) / (TotalRotateTimes - AccumulativeRotateTimes));
+            setImageMatrix(mScaleMatrix);
+            AccumulativeRotateTimes++;
+            AccumulativeRotateAngles += AnglePerTime;
+
+
+            if (AccumulativeRotateTimes < TotalRotateTimes) {
+                postDelayed(this, autoRotationRunnableDelay);
+            } else {
+                currentScaleLevel = getCurrentScaleLevel();
+                currentAbsScaleLevel = Math.abs(currentScaleLevel);
+                calculateImageCenterCoordinates();
+                mScaleMatrix.postRotate(targetRotateAngle - AccumulativeRotateAngles, ImageCenterX, ImageCenterY);
+                if (mAutoRotateCategory == XuanImageViewSettings.AUTO_ROTATE_CATEGORY_RESTORATION) {
+                    mScaleMatrix.postScale(Math.abs(mInitScale / currentScaleLevel), Math.abs(mInitScale / currentScaleLevel), ImageCenterX, ImageCenterY);
+                } else if (mAutoRotateCategory == XuanImageViewSettings.AUTO_ROTATE_CATEGORY_MAGNETISM) {
+                    if (mOrientation == XuanImageViewSettings.ORIENTATION_LANDSCAPE) {
+                        mScaleMatrix.postScale(Math.abs(mInitScale / currentScaleLevel), Math.abs(mInitScale / currentScaleLevel), ImageCenterX, ImageCenterY);
+                    } else if (mOrientation == XuanImageViewSettings.ORIENTATION_PORTRAIT) {
+//                        mScaleMatrix.postScale(Math.abs(mInitPortraitScale / getCurrentScaleLevel()), Math.abs(mInitPortraitScale / getCurrentScaleLevel()), ImageCenterX, ImageCenterY);
+                        mTempInitPortraitScale = Math.abs(currentScaleLevel);
+                        mTempPortraitMaxScale = mTempInitPortraitScale * mMaxScaleMultiple;
+                        mTempPortraitDoubleTabScale = mTempInitPortraitScale * mDoubleTabScaleMultiple;
+                    }
+                }
+                mScaleMatrix.postTranslate(XuanImageViewCenterX - ImageCenterX, XuanImageViewCenterY - ImageCenterY);
+
+                checkBorderAndCenterWhenScale();
+
+                setImageMatrix(mScaleMatrix);
+                isAutoRotated = false;
+            }
+
+
+        }
+    }
+
+    /**
+     * Set a boolean value to determine whether rotation function is turned on.
+     *
+     * @param toggle determine whether rotation function is turned on
+     */
+    public void setRotationToggle(boolean toggle) {
+        mRotationToggle = toggle;
+    }
+
+    /**
+     * @return current RotationToggle
+     */
+    public boolean getRotationToggle() {
+        return mRotationToggle;
+    }
+
+    /**
+     * Set AutoRotateCategory, there are two alternative values of it : XuanImageViewSettings.AUTO_ROTATE_CATEGORY_RESTORATION, XuanImageViewSettings.AUTO_ROTATE_CATEGORY_MAGNETISM.
+     *
+     * @param category
+     */
+    public void setAutoRotateCategory(int category) {
+        if (category == XuanImageViewSettings.AUTO_ROTATE_CATEGORY_RESTORATION || category == XuanImageViewSettings.AUTO_ROTATE_CATEGORY_MAGNETISM)
+            mAutoRotateCategory = category;
+        else
+            mAutoRotateCategory = XuanImageViewSettings.AUTO_ROTATE_CATEGORY_RESTORATION;
+    }
+
+    /**
+     * @return current AutoRotateCategory
+     */
+    public int getAutoRotateCategory() {
+        return mAutoRotateCategory;
+    }
+
+    /**
+     * An image is scaled to an InitScale to fit the size of XuanImageView at the very beginning.
+     * MaxScale = MaxScaleMultiple * InitScale holds.
+     *
+     * @param maxScaleMultiple
+     */
+    public void setMaxScaleMultiple(float maxScaleMultiple) {
+        mMaxScaleMultiple = maxScaleMultiple;
+    }
+
+    /**
+     * @return current MaxScaleMultiple
+     */
+    public float getMaxScaleMultiple() {
+        return mMaxScaleMultiple;
+    }
+
+    /**
+     * When image's current scale level is smaller than DoubleTabScale, the image will scale up to DoubleTapScale if an double-tap gesture is detected.
+     * DoubleTapScale = DoubleTabScaleMultiple * InitScale holds.
+     *
+     * @param doubleTabScaleMultiple
+     */
+    public void setDoubleTabScaleMultiple(float doubleTabScaleMultiple) {
+        mDoubleTabScaleMultiple = doubleTabScaleMultiple;
+    }
+
+    /**
+     * @return current DoubleTabScaleMultiple
+     */
+    public float getDoubleTabScaleMultiple() {
+        return mDoubleTabScaleMultiple;
+    }
+
+    /**
+     * If current scale level is smaller than InitScale and image is not in rotation state,
+     * the image will scale up to InitScale with SpringBackGradientScaleUpLevel step by step.
+     * Default springBackGradientScaleUpLevel is  1.01f.
+     *
+     * @param springBackGradientScaleUpLevel
+     */
+    public void setSpringBackGradientScaleUpLevel(float springBackGradientScaleUpLevel) {
+        mSpringBackGradientScaleUpLevel = springBackGradientScaleUpLevel;
+    }
+
+    /**
+     * @return current SpringBackGradientScaleUpLevel
+     */
+    public float getSpringBackGradientScaleUpLevel() {
+        return mSpringBackGradientScaleUpLevel;
+    }
+
+    /**
+     * If current scale level is bigger than MaxScale and image is not in rotation state,
+     * the image will scale down to MaxScale with SpringBackGradientScaleDownLevel step by step.
+     * Default springBackGradientScaleDownLevel is 0.99f.
+     *
+     * @param springBackGradientScaleDownLevel
+     */
+    public void setSpringBackGradientScaleDownLevel(float springBackGradientScaleDownLevel) {
+        mSpringBackGradientScaleDownLevel = springBackGradientScaleDownLevel;
+    }
+
+    /**
+     * @return current SpringBackGradientScaleDownLevel
+     */
+    public float getSpringBackGradientScaleDownLevel() {
+        return mSpringBackGradientScaleDownLevel;
+    }
+
+    /**
+     * When image's current scale level is smaller than DoubleTabScale,
+     * the image will scale up to DoubleTapScale with DoubleTapGradientScaleUpLevel step by step if a double-tap gesture is detected.
+     * Default doubleTalGradientScaleUpLevel is 1.05f.
+     *
+     * @param doubleTapGradientScaleUpLevel
+     */
+    public void setDoubleTapGradientScaleUpLevel(float doubleTapGradientScaleUpLevel) {
+        mDoubleTapGradientScaleUpLevel = doubleTapGradientScaleUpLevel;
+    }
+
+    /**
+     * @return current DoubleTapGradientScaleUpLevel
+     */
+    public float getDoubleTapGradientScaleUpLevel() {
+        return mDoubleTapGradientScaleUpLevel;
+    }
+
+    /**
+     * When image's current scale level is bigger than DoubleTabScale,
+     * the image will scale down to InitScale with DoubleTapGradientScaleDownLevel step by step if a double-tap gesture is detected.
+     * Default doubleTabGradientScaleDownLevel is 0.95f.
+     *
+     * @param doubleTapGradientScaleDownLevel
+     */
+    public void setDoubleTabGradientScaleDownLevel(float doubleTapGradientScaleDownLevel) {
+        mDoubleTapGradientScaleDownLevel = doubleTapGradientScaleDownLevel;
+    }
+
+    /**
+     * @return current DoubleTapGradientScaleDownLevel
+     */
+    public float getDoubleTapGradientScaleDownLevel() {
+        return mDoubleTapGradientScaleDownLevel;
+    }
+
+    /**
+     * When image's current rotation angle is bigger than AutoRotationTrigger, the image will rotate in the same direction and scale back to it's initial state if rotation gesture is released.
+     * When image's current rotation angle is smaller than AutoRotationTrigger, the image will rotate in the opposite direction and scale back to it's initial state if rotation gesture is released.
+     * Default AutoRotationTrigger is 60 (degrees).
+     *
+     * @param autoRotationTrigger
+     */
+    public void setAutoRotationTrigger(float autoRotationTrigger) {
+        this.autoRotationTrigger = autoRotationTrigger;
+    }
+
+    /**
+     * @return current AutoRotationTrigger
+     */
+    public float getAutoRotationTrigger() {
+        return autoRotationTrigger;
+    }
+
+    /**
+     * Default SpringBackRunnableDelay is 10 (milliseconds).
+     *
+     * @param delay
+     */
+    public void setSpringBackRunnableDelay(int delay) {
+        springBackRunnableDelay = delay;
+    }
+
+    /**
+     * @return current SpringBackRunnableDelay
+     */
+    public int getSpringBackRunnableDelay() {
+        return springBackRunnableDelay;
+    }
+
+    /**
+     * Default DoubleTapRunnableDelay is 10 (milliseconds).
+     *
+     * @param delay
+     */
+    public void setDoubleTapScaleRunnableDelay(int delay) {
+        doubleTabScaleRunnableDelay = delay;
+    }
+
+    /**
+     * @return current DoubleTabScaleRunnableDelay
+     */
+    public int getDoubleTabScaleRunnableDelay() {
+        return doubleTabScaleRunnableDelay;
+    }
+
+    /**
+     * Default AutoRotationRunnableDelay is 5 (milliseconds).
+     *
+     * @param delay
+     */
+    public void setAutoRotationRunnableDelay(int delay) {
+        autoRotationRunnableDelay = delay;
+    }
+
+    /**
+     * @return current AutoRotationRunnableDelay
+     */
+    public int getAutoRotationRunnalbleDelay() {
+        return autoRotationRunnableDelay;
+    }
+
+    /**
+     * Default AutoRotationRunnableTimes is 10 (times).
+     *
+     * @param times
+     */
+    public void setAutoRotationRunnableTimes(int times) {
+        autoRotationRunnableTimes = times;
+    }
+
+    /**
+     * @return current AutoRotationRunnableTimes
+     */
+    public int getAutoRotationRunnableTimes() {
+        return autoRotationRunnableTimes;
+    }
+
+
+    /**
+     * Notice the Image can only start to be rotated when it's in initial state. But the image may be scaled up or down a little bit by ScaleGestureDetector
+     * in advance when you try to rotate it, hence, currentScaleLevel is not precisely equal to initScaleLevel. Here, an AllowableFloatError is existed to handle
+     * this situation. When Math.abs(currentScaleLevel - initScaleLevel) is smaller than allowableFloatError, RotateGestureDetector.onTouchEvent() can be invoked.
+     * Default allowableFloatError is 1E-6, it should be compatible with most of devices. For devices whose display resolution and aspect ratio is not normal, allowableFloatError may
+     * need to be tuned. eg., for Galaxy S8, 3E-3 works well. Of course, 3E-3 also works for most of devices because 1E-6 is smaller than 3E-3.
+     * @param allowableFloatError
+     */
+    public void setAllowableFloatError(double allowableFloatError){
+        this.allowableFloatError = allowableFloatError;
+    }
+
+    public double getAllowableFloatError(){
+        return allowableFloatError;
+    }
+
+    /**
+     * In  AUTO_ROTATE_CATEGORY_MAGNETISM mode, the image may be showed under a fixed rotation angle like 90 degrees, 270 degrees,
+     * 450 degrees, ect., then allowablePortraitFloatError should handle the situation when currentPortraitScaleLevel is not precisely
+     * equal to initPortraitScaleLevel.
+     * Default allowablePortraitFloatError is 1E-12, it should be compatible with most of devices. For devices whose display resolution and aspect ratio is not normal, allowablePortraitFloatError may
+     * need to be tuned. eg., for Galaxy S8, 5E-8 works well. Of course, 5E-8 also works for most of devices because 1E-12 is smaller than 5E-8.
+     * @see XuanImageView#setAllowableFloatError(double)
+     *
+     * @param allowablePortraitFloatError
+     */
+    public void setAllowablePortraitFloatError(double allowablePortraitFloatError){
+        this.allowablePortraitFloatError = allowablePortraitFloatError;
+    }
+
+    public double getAllowablePortraitFloatError(){
+        return allowablePortraitFloatError;
+    }
+
+}

+ 13 - 0
RA Image/app/src/main/java/com/usai/redant/raimage/PhotoList/XuanImageView/XuanImageViewSettings.java

@@ -0,0 +1,13 @@
+package com.usai.redant.raimage.PhotoList.XuanImageView;
+
+/**
+ * Created by xuanyihuang on 20/12/2016.
+ */
+
+public class XuanImageViewSettings {
+    public static int ORIENTATION_PORTRAIT = 1;
+    public static int ORIENTATION_LANDSCAPE = 2;
+
+    public static int AUTO_ROTATE_CATEGORY_RESTORATION = 1;
+    public static int AUTO_ROTATE_CATEGORY_MAGNETISM = 2;
+}

+ 13 - 3
RA Image/app/src/main/res/layout/activity_new_photo_preview.xml

@@ -2,19 +2,29 @@
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:background="#ff0000"
+    android:background="#000000"
     >
 
 
 
-    <com.usai.redant.raimage.PhotoList.GridLayoutRecycler.RAPreviewRecyclerView
+    <!--<com.usai.redant.raimage.PhotoList.GridLayoutRecycler.RAPreviewRecyclerView-->
+        <!--android:id="@+id/preview_recycler"-->
+        <!--android:layout_width="match_parent"-->
+        <!--android:layout_height="match_parent"-->
+        <!--android:layout_alignParentTop="true"-->
+        <!--android:layout_alignParentLeft="true"-->
+        <!--android:layout_alignParentStart="true"-->
+        <!--android:background="#aaff00"/>-->
+
+    <android.support.v4.view.ViewPager
         android:id="@+id/preview_recycler"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:layout_alignParentTop="true"
         android:layout_alignParentLeft="true"
         android:layout_alignParentStart="true"
-        android:background="#aaff00"/>
+        android:background="#000000"
+        />
 
     <TextView
         android:id="@+id/index_tv"

+ 2 - 2
RA Image/app/src/main/res/layout/photo_preview_item.xml

@@ -2,9 +2,9 @@
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="vertical" android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:background="#0aa">
+    >
 
-    <ImageView
+    <com.usai.redant.raimage.PhotoList.XuanImageView.XuanImageView
         android:id="@+id/preview_item"
         android:layout_width="match_parent"
         android:layout_height="match_parent"

+ 20 - 0
RA Image/app/src/main/res/values-w820dp/attrs_xuanimageview.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <declare-styleable name="xuanimageview">
+        <attr name="RotationToggle" format="boolean"/>
+        <attr name="AutoRotateCategory" format="integer"/>
+        <attr name="MaxScaleMultiple" format="float"/>
+        <attr name="DoubleTabScaleMultiple" format="float"/>
+        <attr name="AllowableFloatError" format="string"/>
+        <attr name="AllowablePortraitFloatError" format="string"/>
+        <attr name="SpringBackGradientScaleUpLevel" format="float"/>
+        <attr name="SpringBackGradientScaleDownLevel" format="float"/>
+        <attr name="DoubleTapGradientScaleUpLevel" format="float"/>
+        <attr name="DoubleTapGradientScaleDownLevel" format="float"/>
+        <attr name="AutoRotationTrigger" format="float"/>
+        <attr name="SpringBackRunnableDelay" format="integer"/>
+        <attr name="DoubleTapScaleRunnableDelay" format="integer"/>
+        <attr name="AutoRotationRunnableDelay" format="integer"/>
+        <attr name="AutoRotationRunnableTimes" format="integer"/>
+    </declare-styleable>
+</resources>