zengjiebin 8 gadi atpakaļ
vecāks
revīzija
e86317aa44
100 mainītis faili ar 62101 papildinājumiem un 155 dzēšanām
  1. 1 0
      .idea/gradle.xml
  2. 5 3
      app/build.gradle
  3. 4 1
      app/src/main/AndroidManifest.xml
  4. 11 16
      app/src/main/java/com/sheep/gamegroup/model/api/ApiService.java
  5. 62 0
      app/src/main/java/com/sheep/gamegroup/model/api/BaseMessageConverter.java
  6. 5 1
      app/src/main/java/com/sheep/gamegroup/model/api/StringConverterFactory.java
  7. 34 2
      app/src/main/java/com/sheep/gamegroup/model/util/AddPuplicParameIntercept.java
  8. 2 1
      app/src/main/java/com/sheep/gamegroup/model/util/SheepSubscriber.java
  9. 3 6
      app/src/main/java/com/sheep/gamegroup/presenter/LoginPresenter.java
  10. 10 0
      app/src/main/java/com/sheep/gamegroup/util/AppUtil.java
  11. 0 10
      app/src/main/java/com/sheep/gamegroup/view/activity/AccountAndSecurityAct.java
  12. 5 3
      app/src/main/java/com/sheep/gamegroup/view/activity/AskGetMoneyAct.java
  13. 0 1
      app/src/main/java/com/sheep/gamegroup/view/activity/BindDataAct.java
  14. 0 7
      app/src/main/java/com/sheep/gamegroup/view/activity/BindOrChangeWeixinAct.java
  15. 0 1
      app/src/main/java/com/sheep/gamegroup/view/activity/ChangeTelAct.java
  16. 0 1
      app/src/main/java/com/sheep/gamegroup/view/activity/ChangeWxOrTelAct.java
  17. 0 1
      app/src/main/java/com/sheep/gamegroup/view/activity/CommitWxAct.java
  18. 0 1
      app/src/main/java/com/sheep/gamegroup/view/activity/CommitWxCodeAct.java
  19. 0 1
      app/src/main/java/com/sheep/gamegroup/view/activity/HomePageAct.java
  20. 0 1
      app/src/main/java/com/sheep/gamegroup/view/activity/LieMakeMoneyAct.java
  21. 8 6
      app/src/main/java/com/sheep/gamegroup/view/activity/PersionInfoAct.java
  22. 0 2
      app/src/main/java/com/sheep/gamegroup/view/activity/PersonalCenterAct.java
  23. 0 9
      app/src/main/java/com/sheep/gamegroup/view/activity/PhoneAct.java
  24. 4 8
      app/src/main/java/com/sheep/gamegroup/view/activity/ReNameActivity.java
  25. 0 3
      app/src/main/java/com/sheep/gamegroup/view/activity/RealNameAuthenAct.java
  26. 0 7
      app/src/main/java/com/sheep/gamegroup/view/activity/SplashAct.java
  27. 0 1
      app/src/main/java/com/sheep/gamegroup/view/activity/TaskListAct.java
  28. 0 1
      app/src/main/java/com/sheep/gamegroup/view/activity/TryMakeMoneyact.java
  29. 0 1
      app/src/main/java/com/sheep/gamegroup/view/activity/WithdrawalAct.java
  30. 0 1
      app/src/main/java/com/sheep/gamegroup/view/activity/WithdrawalListAct.java
  31. 0 7
      app/src/main/java/com/sheep/gamegroup/view/activity/WithdrawalResultAct.java
  32. 2 3
      app/src/main/java/com/sheep/jiuyan/samllsheep/BaseApplication.java
  33. 50 11
      app/src/main/java/com/sheep/jiuyan/samllsheep/page/AskFriendActivity.java
  34. 2 4
      app/src/main/res/layout/x_ask_qr_dialog.xml
  35. 4 12
      app/src/main/res/layout/x_ask_reward_dialog.xml
  36. 2 13
      app/src/main/res/layout/x_rename_act_layout.xml
  37. 9 4
      app/src/main/res/layout/xpersion_info_act_layout.xml
  38. 3 0
      build.gradle
  39. 0 3
      datashare/build.gradle
  40. 12 1
      gradle.properties
  41. 91 0
      mavenpush.gradle
  42. 1 1
      settings.gradle
  43. 1 0
      ucrop/.gitignore
  44. 41 0
      ucrop/build.gradle
  45. 3 0
      ucrop/gradle.properties
  46. 17 0
      ucrop/proguard-rules.pro
  47. 2 0
      ucrop/src/main/AndroidManifest.xml
  48. 538 0
      ucrop/src/main/java/com/yalantis/ucrop/UCrop.java
  49. 643 0
      ucrop/src/main/java/com/yalantis/ucrop/UCropActivity.java
  50. 12 0
      ucrop/src/main/java/com/yalantis/ucrop/callback/BitmapCropCallback.java
  51. 15 0
      ucrop/src/main/java/com/yalantis/ucrop/callback/BitmapLoadCallback.java
  52. 10 0
      ucrop/src/main/java/com/yalantis/ucrop/callback/CropBoundsChangeListener.java
  53. 12 0
      ucrop/src/main/java/com/yalantis/ucrop/callback/OverlayViewChangeListener.java
  54. 66 0
      ucrop/src/main/java/com/yalantis/ucrop/model/AspectRatio.java
  55. 58 0
      ucrop/src/main/java/com/yalantis/ucrop/model/CropParameters.java
  56. 63 0
      ucrop/src/main/java/com/yalantis/ucrop/model/ExifInfo.java
  57. 37 0
      ucrop/src/main/java/com/yalantis/ucrop/model/ImageState.java
  58. 195 0
      ucrop/src/main/java/com/yalantis/ucrop/task/BitmapCropTask.java
  59. 280 0
      ucrop/src/main/java/com/yalantis/ucrop/task/BitmapLoadTask.java
  60. 173 0
      ucrop/src/main/java/com/yalantis/ucrop/util/BitmapLoadUtils.java
  61. 17 0
      ucrop/src/main/java/com/yalantis/ucrop/util/CubicEasing.java
  62. 134 0
      ucrop/src/main/java/com/yalantis/ucrop/util/EglUtils.java
  63. 103 0
      ucrop/src/main/java/com/yalantis/ucrop/util/FastBitmapDrawable.java
  64. 236 0
      ucrop/src/main/java/com/yalantis/ucrop/util/FileUtils.java
  65. 425 0
      ucrop/src/main/java/com/yalantis/ucrop/util/ImageHeaderParser.java
  66. 72 0
      ucrop/src/main/java/com/yalantis/ucrop/util/RectUtils.java
  67. 111 0
      ucrop/src/main/java/com/yalantis/ucrop/util/RotationGestureDetector.java
  68. 42 0
      ucrop/src/main/java/com/yalantis/ucrop/util/SelectedStateListDrawable.java
  69. 627 0
      ucrop/src/main/java/com/yalantis/ucrop/view/CropImageView.java
  70. 152 0
      ucrop/src/main/java/com/yalantis/ucrop/view/GestureCropImageView.java
  71. 577 0
      ucrop/src/main/java/com/yalantis/ucrop/view/OverlayView.java
  72. 339 0
      ucrop/src/main/java/com/yalantis/ucrop/view/TransformImageView.java
  73. 81 0
      ucrop/src/main/java/com/yalantis/ucrop/view/UCropView.java
  74. 163 0
      ucrop/src/main/java/com/yalantis/ucrop/view/widget/AspectRatioTextView.java
  75. 151 0
      ucrop/src/main/java/com/yalantis/ucrop/view/widget/HorizontalProgressWheelView.java
  76. 14 0
      ucrop/src/main/jni/Android.mk
  77. 6 0
      ucrop/src/main/jni/Application.mk
  78. 56164 0
      ucrop/src/main/jni/CImg.h
  79. 22 0
      ucrop/src/main/jni/com_yalantis_ucrop_task_BitmapCropTask.h
  80. 116 0
      ucrop/src/main/jni/uCrop.cpp
  81. BIN
      ucrop/src/main/jniLibs/arm64-v8a/libucrop.so
  82. BIN
      ucrop/src/main/jniLibs/armeabi-v7a/libucrop.so
  83. BIN
      ucrop/src/main/jniLibs/armeabi/libucrop.so
  84. BIN
      ucrop/src/main/jniLibs/x86/libucrop.so
  85. BIN
      ucrop/src/main/jniLibs/x86_64/libucrop.so
  86. 20 0
      ucrop/src/main/res/anim/ucrop_loader_circle_path.xml
  87. 28 0
      ucrop/src/main/res/anim/ucrop_loader_circle_scale.xml
  88. 5 0
      ucrop/src/main/res/color/ucrop_scale_text_view_selector.xml
  89. BIN
      ucrop/src/main/res/drawable-hdpi/ucrop_ic_angle.png
  90. BIN
      ucrop/src/main/res/drawable-hdpi/ucrop_ic_crop.png
  91. BIN
      ucrop/src/main/res/drawable-hdpi/ucrop_ic_cross.png
  92. BIN
      ucrop/src/main/res/drawable-hdpi/ucrop_ic_done.png
  93. BIN
      ucrop/src/main/res/drawable-hdpi/ucrop_ic_next.png
  94. BIN
      ucrop/src/main/res/drawable-hdpi/ucrop_ic_reset.png
  95. BIN
      ucrop/src/main/res/drawable-hdpi/ucrop_ic_rotate.png
  96. BIN
      ucrop/src/main/res/drawable-hdpi/ucrop_ic_scale.png
  97. BIN
      ucrop/src/main/res/drawable-ldpi/ucrop_ic_angle.png
  98. BIN
      ucrop/src/main/res/drawable-ldpi/ucrop_ic_crop.png
  99. BIN
      ucrop/src/main/res/drawable-ldpi/ucrop_ic_cross.png
  100. 0 0
      ucrop/src/main/res/drawable-ldpi/ucrop_ic_done.png

+ 1 - 0
.idea/gradle.xml

@@ -10,6 +10,7 @@
             <option value="$PROJECT_DIR$" />
             <option value="$PROJECT_DIR$/app" />
             <option value="$PROJECT_DIR$/datashare" />
+            <option value="$PROJECT_DIR$/ucrop" />
             <option value="$PROJECT_DIR$/view" />
           </set>
         </option>

+ 5 - 3
app/build.gradle

@@ -29,6 +29,8 @@ android {
             keyPassword 'zhaoyi2004'
             storeFile file('.././sign.jks')
             storePassword 'zhaoyi2004'
+            v1SigningEnabled true
+            v2SigningEnabled false
         }
 
     }
@@ -41,7 +43,6 @@ android {
     dexOptions {
         preDexLibraries = false
         javaMaxHeapSize "4g"
-        incremental true
     }
     packagingOptions {
 
@@ -138,7 +139,7 @@ dependencies {
     }
     compile 'org.xutils:xutils:3.5.0'
     compile 'com.mylhyl:acp:1.1.7'
-    compile 'cn.yipianfengye.android:zxing-library:2.2'
+//    compile 'cn.yipianfengye.android:zxing-library:2.2'
     compile 'com.github.huburt-Hu:NewbieGuide:v1.2.0'
     debugCompile 'com.android.support:multidex:1.0.1'//不要修改这里,相信我
     releaseCompile 'com.android.support:multidex:1.0.3'
@@ -221,6 +222,7 @@ dependencies {
     compile 'com.github.zhangkexpz:LayoutScroll:v1.0'
     compile 'me.iwf.photopicker:PhotoPicker:0.9.12@aar'
     compile 'com.github.bumptech.glide:glide:4.1.1'
-    compile 'com.github.yalantis:ucrop:2.2.1-native'
+//    compile 'com.github.yalantis:ucrop:2.2.1-native'
+    compile project(':ucrop')
 
 }

+ 4 - 1
app/src/main/AndroidManifest.xml

@@ -351,10 +351,12 @@
 
 
         <activity android:name="com.sheep.gamegroup.view.activity.CommitWxAct"
+            android:windowSoftInputMode="stateVisible|adjustPan"
             android:screenOrientation="portrait" />
         <activity android:name="com.sheep.gamegroup.view.activity.ActGuideOnHook"
             android:screenOrientation="portrait" />
         <activity android:name="com.sheep.gamegroup.view.activity.CommitWxCodeAct"
+            android:windowSoftInputMode="stateVisible|adjustPan"
             android:screenOrientation="portrait" />
         <activity android:name="com.sheep.gamegroup.view.activity.ActGuideDeblocked"
             android:screenOrientation="portrait" />
@@ -364,6 +366,7 @@
         <activity android:name="com.sheep.gamegroup.view.activity.ChangeWxOrTelAct"
             android:screenOrientation="portrait"/>
         <activity android:name="com.sheep.gamegroup.view.activity.ReNameActivity"
+            android:windowSoftInputMode="stateVisible|adjustPan"
             android:screenOrientation="portrait"/>
         <activity android:name="me.iwf.photopicker.PhotoPickerActivity"
             android:theme="@style/Theme.AppCompat.NoActionBar"
@@ -385,7 +388,7 @@
         <!--友盟start-->
         <meta-data
             android:name="UMENG_APPKEY"
-            android:value="5ab0a1da8f4a9d742900035f"></meta-data>
+            android:value="5ab0a1da8f4a9d742900035f"/>
         <meta-data android:value="xxx" android:name="UMENG_CHANNEL"/>
 
     </application>

+ 11 - 16
app/src/main/java/com/sheep/gamegroup/model/api/ApiService.java

@@ -1,25 +1,20 @@
 package com.sheep.gamegroup.model.api;
 
-import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.sheep.gamegroup.model.entity.BaseMessage;
-import com.sheep.gamegroup.util.ConnectAddress;
 
 
 import java.util.Map;
 
 import retrofit2.Call;
 import retrofit2.http.Body;
-import retrofit2.http.Field;
 import retrofit2.http.FieldMap;
 import retrofit2.http.FormUrlEncoded;
 import retrofit2.http.GET;
-import retrofit2.http.Header;
 import retrofit2.http.POST;
 import retrofit2.http.PUT;
 import retrofit2.http.Path;
 import retrofit2.http.Query;
-import retrofit2.http.QueryMap;
 import retrofit2.http.Streaming;
 import rx.Observable;
 
@@ -124,13 +119,13 @@ public interface ApiService {
     /**
      * 获取自己正在运行的任务
      */
-    @GET(V1 + "/app/accepted_task/run_task/")
+    @GET(V1 + "app/accepted_task/run_task/")
     Observable<BaseMessage> returnTask();
 
     /**
      * 获取正在运行的任务
      */
-    @GET(V1 + "/app/release_task/")
+    @GET(V1 + "app/release_task/")
     Observable<BaseMessage> releaseTask();
 
 
@@ -138,7 +133,7 @@ public interface ApiService {
     /**
      * 邀请赚钱
      */
-    @GET(V1+"/app/auth/login_by_we_chat")
+    @GET(V1+"app/auth/login_by_we_chat")
     Observable<BaseMessage> askMakeMoney(@Body String jsonObject);
     /**
      * 用户提现记录
@@ -152,7 +147,7 @@ public interface ApiService {
      * 获取正在运行的任务
      * @param jsonObject
      */
-    @POST(V1+"/app/auth/login_by_we_chat")
+    @POST(V1+"app/auth/login_by_we_chat")
     Observable<BaseMessage> LoginByWX(@Body com.alibaba.fastjson.JSONObject jsonObject);
 
 
@@ -161,12 +156,12 @@ public interface ApiService {
      * 获取正在运行的任务
      * @param jsonObject
      */
-    @POST(V1+"/app/auth/login_by_qq")
+    @POST(V1+"app/auth/login_by_qq")
     Observable<BaseMessage> LoginByQQ(@Body com.alibaba.fastjson.JSONObject jsonObject);
 
 
 
-    @GET(V1+"/app/user/friend_count_and_award")
+    @GET(V1+"app/user/friend_count_and_award")
     Observable<BaseMessage> FriendCountAndAward();
 
     /**
@@ -178,18 +173,18 @@ public interface ApiService {
     /**
      * 获取任务显示状态
      */
-    @GET(V1 + "/app/task_log/node/{id}")
+    @GET(V1 + "app/task_log/node/{id}")
     Observable<BaseMessage> giveTaskStatue(@Path("id") int id);
 
 
 
-    @PUT(V1+"/app/user/change_base_info")
+    @PUT(V1+"app/user/change_base_info")
     Observable<BaseMessage> changeBaseInfo (@Body com.alibaba.fastjson.JSONObject jsonObject);
 
     /**
      * 更换手机号
      */
-    @POST(V1 + "/app/user/switch_phone")
+    @POST(V1 + "app/user/switch_phone")
     Observable<BaseMessage> switchPhone(@Body JSONObject jsonObject);
 
 
@@ -197,7 +192,7 @@ public interface ApiService {
     /**
      * 更换手机号
      */
-    @PUT(V1 + "/app/user/bind_all_real_info")
+    @PUT(V1 + "app/user/bind_all_real_info")
     Observable<BaseMessage> bindALl(@Body JSONObject jsonObject);
     /**
      * 绑定手机号
@@ -208,7 +203,7 @@ public interface ApiService {
     /**
      * 更换手机号
      */
-    @GET(V1 + "/app/user/award_detail")
+    @GET(V1 + "app/user/award_detail")
     Observable<BaseMessage> awardDetail(@Query("page") int page,@Query("per_page") int per_page);
     /**
      * 发送绑定手机号短信,只传mobile

+ 62 - 0
app/src/main/java/com/sheep/gamegroup/model/api/BaseMessageConverter.java

@@ -0,0 +1,62 @@
+package com.sheep.gamegroup.model.api;
+
+import com.alibaba.fastjson.JSONObject;
+import com.kfzs.appstore.utils.string.HexUtils;
+import com.kfzs.duanduan.proto.ApiResponseOuterClass;
+import com.sheep.gamegroup.model.entity.BaseMessage;
+
+import java.io.IOException;
+
+import go.kfzssafe.Kfzssafe;
+import okhttp3.ResponseBody;
+import retrofit2.Converter;
+
+/**
+ * Created by realicing on 2018/4/2.
+ */
+public class BaseMessageConverter implements Converter<ResponseBody, BaseMessage> {
+
+    @Override
+    public BaseMessage convert(ResponseBody value) throws IOException {
+        String json = decrypt(value.string());
+        return JSONObject.parseObject(json, BaseMessage.class);
+    }
+
+
+    /**
+     * 加密
+     * @param requestJson
+     * @return
+     */
+    public static String encrypt(String requestJson){
+        try {
+            byte[] byteEncode = Kfzssafe.XByteEncode(requestJson.getBytes());
+            return HexUtils.bytes2HexStr(byteEncode);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return requestJson;
+        }
+    }
+
+    /**
+     * 解密
+     * @param respenseJson
+     * @return
+     */
+    public static String decrypt(String respenseJson){
+        if(respenseJson.startsWith("{")){
+            return respenseJson;
+        }
+        try {
+            byte[] bytes = HexUtils.hexStr2Bytes(respenseJson);
+            byte[] byteDecode = Kfzssafe.XByteDecode(bytes);
+//            ApiResponseOuterClass.ApiResponse apiResponse = ApiResponseOuterClass.ApiResponse.parseFrom(byteDecode);
+            String string = new String(byteDecode);
+
+            return string;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return respenseJson;
+        }
+    }
+}

+ 5 - 1
app/src/main/java/com/sheep/gamegroup/model/api/StringConverterFactory.java

@@ -1,5 +1,7 @@
 package com.sheep.gamegroup.model.api;
 
+import com.sheep.gamegroup.model.entity.BaseMessage;
+
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
 
@@ -20,8 +22,10 @@ public class StringConverterFactory extends Converter.Factory {
     public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
         if (type == String.class) {
             return new StringConverter();
+        } else if(type == BaseMessage.class) {
+            //其它类型我们不处理,返回null就行
+            return new BaseMessageConverter();
         }
-        //其它类型我们不处理,返回null就行
         return null;
     }
 }

+ 34 - 2
app/src/main/java/com/sheep/gamegroup/model/util/AddPuplicParameIntercept.java

@@ -1,14 +1,26 @@
 package com.sheep.gamegroup.model.util;
 
+import android.text.TextUtils;
+
+import com.google.gson.Gson;
+import com.kfzs.appstore.utils.string.HexUtils;
+import com.sheep.gamegroup.model.api.BaseMessageConverter;
+import com.sheep.gamegroup.model.api.StringConverter;
+import com.sheep.gamegroup.model.entity.BaseMessage;
 import com.sheep.jiuyan.samllsheep.SheepApp;
 import com.sheep.jiuyan.samllsheep.utils.SpUtils;
 
 import java.io.IOException;
+import java.util.HashMap;
 
+import go.kfzssafe.Kfzssafe;
+import okhttp3.FormBody;
 import okhttp3.HttpUrl;
 import okhttp3.Interceptor;
 import okhttp3.Request;
+import okhttp3.RequestBody;
 import okhttp3.Response;
+import okio.Buffer;
 
 /**
  * http 请求添加公共的参数
@@ -39,9 +51,29 @@ public class AddPuplicParameIntercept implements Interceptor {
     private Request addParam(Request oldRequest) {
         HttpUrl.Builder b = oldRequest.url()
                 .newBuilder();
+        String method = oldRequest.method();
+        RequestBody body = oldRequest.body();
+        if (method.equals("POST") || method.equals("PUT") ) {
+            RequestBody requestBody = oldRequest.body();
+            if (requestBody instanceof FormBody) {
+            } else {
+                //buffer流
+                Buffer buffer = new Buffer();
+                try {
+                    requestBody.writeTo(buffer);
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+                String oldParamsJson = buffer.readUtf8();
+                String newJsonParams = BaseMessageConverter.encrypt(oldParamsJson);
+                body = RequestBody.create(requestBody.contentType(), newJsonParams);
+//                oldRequest = oldRequest.newBuilder().post(body).build();
+            }
+        }
+        String id = SpUtils.getOpenId(SheepApp.mContext);
         Request r = oldRequest.newBuilder()
-                .addHeader("Authorization", SpUtils.getOpenId(SheepApp.mContext)+"")//header 会覆盖以前的  header
-                .method(oldRequest.method(), oldRequest.body())
+                .addHeader("Authorization", TextUtils.isEmpty(id) ? "" : BaseMessageConverter.encrypt(id))//header 会覆盖以前的  header
+                .method(method, body)
                 .url(b.build())
                 .build();
         return r;

+ 2 - 1
app/src/main/java/com/sheep/gamegroup/model/util/SheepSubscriber.java

@@ -5,6 +5,7 @@ import android.util.Log;
 import android.widget.Toast;
 
 import com.alibaba.fastjson.JSONObject;
+import com.sheep.gamegroup.model.api.BaseMessageConverter;
 import com.sheep.gamegroup.model.entity.BaseMessage;
 import com.sheep.gamegroup.util.ActivityManager;
 import com.sheep.gamegroup.util.Jump2View;
@@ -62,7 +63,7 @@ public abstract class SheepSubscriber<T> extends Subscriber<T> {
                     Jump2View.getInstance().goLoginView(SheepApp.mContext, "");
                     return;
                 }
-                BaseMessage baseMessage = JSONObject.parseObject(throwable.message, BaseMessage.class);
+                BaseMessage baseMessage = JSONObject.parseObject(BaseMessageConverter.decrypt(throwable.message), BaseMessage.class);
                 if(baseMessage.getCode() == 0)
                     baseMessage.setCode(throwable.code);
                 onError(baseMessage);

+ 3 - 6
app/src/main/java/com/sheep/gamegroup/presenter/LoginPresenter.java

@@ -12,6 +12,7 @@ import com.sheep.gamegroup.model.api.ApiService;
 import com.sheep.gamegroup.model.entity.BaseMessage;
 import com.sheep.gamegroup.model.entity.LoginEntity;
 import com.sheep.gamegroup.model.entity.UserEntity;
+import com.sheep.gamegroup.model.util.SheepSubscriber;
 import com.sheep.gamegroup.util.ChannelContent;
 import com.sheep.gamegroup.util.FastJsonUtils;
 import com.sheep.gamegroup.util.MyDbManager;
@@ -89,14 +90,10 @@ public class LoginPresenter implements LoginContract.Presenter {
         }else{
             apiService.LoginByQQ(j).subscribeOn(Schedulers.io())
                     .observeOn(AndroidSchedulers.mainThread())
-                    .subscribe(new Observer<BaseMessage>() {
-                        @Override
-                        public void onCompleted() {
-
-                        }
+                    .subscribe(new SheepSubscriber<BaseMessage>(SheepApp.mContext) {
 
                         @Override
-                        public void onError(Throwable throwable) {
+                        public void onError(BaseMessage baseMessage) {
                             view.NetError(-1, "QQ登录失败  请检查网络是否正常");
                         }
 

+ 10 - 0
app/src/main/java/com/sheep/gamegroup/util/AppUtil.java

@@ -12,6 +12,15 @@ import android.view.WindowManager;
  * Created by ljy on 2018/3/8.
  */
 public class AppUtil {
+    public static final String BASE_QR = "http://qr.liantu.com/api.php?text=";
+
+    public static String getQRLink(String share_link) {
+        return BASE_QR + share_link;
+    }
+
+    public static String getQRLink(String share_link,int w) {
+        return BASE_QR + share_link +"&w="+w;
+    }
     /**
      * 获取app版本名
      */
@@ -84,4 +93,5 @@ public class AppUtil {
     {
         return (WindowManager)paramContext.getSystemService(Context.WINDOW_SERVICE);
     }
+
 }

+ 0 - 10
app/src/main/java/com/sheep/gamegroup/view/activity/AccountAndSecurityAct.java

@@ -280,16 +280,6 @@ public class AccountAndSecurityAct extends BaseActivity implements AccountAndSec
 
     }
 
-
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        // TODO: add setContentView(...) invocation
-        ButterKnife.bind(this);
-    }
-
-
     @Override
     protected void onDestroy() {
         super.onDestroy();

+ 5 - 3
app/src/main/java/com/sheep/gamegroup/view/activity/AskGetMoneyAct.java

@@ -8,6 +8,7 @@ import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 
+import com.bumptech.glide.Glide;
 import com.sheep.gamegroup.absBase.BaseUMActivity;
 import com.sheep.gamegroup.di.components.DaggerAskGetMoneyComponent;
 import com.sheep.gamegroup.di.modules.AskGetMoneyModule;
@@ -15,6 +16,7 @@ import com.sheep.gamegroup.model.entity.FriendAndAwardEntity;
 import com.sheep.gamegroup.presenter.AskGetMoneyContract;
 import com.sheep.gamegroup.presenter.AskGetMoneyPresenter;
 
+import com.sheep.gamegroup.util.AppUtil;
 import com.sheep.gamegroup.util.StringUtils;
 import com.sheep.jiuyan.samllsheep.R;
 import com.sheep.jiuyan.samllsheep.SheepApp;
@@ -25,7 +27,6 @@ import com.umeng.socialize.UMShareListener;
 import com.umeng.socialize.bean.SHARE_MEDIA;
 import com.umeng.socialize.media.UMImage;
 import com.umeng.socialize.media.UMWeb;
-import com.uuzuche.lib_zxing.activity.CodeUtils;
 
 import javax.inject.Inject;
 
@@ -117,8 +118,9 @@ public class AskGetMoneyAct extends BaseUMActivity implements UMShareListener, A
 //        dialog.getWindow().setBackgroundDrawable(new ColorDrawable(0));
         ImageView iv=view.findViewById(R.id.iv_close);
         ImageView iv_qr=view.findViewById(R.id.iv_qr);
-         Bitmap mBitmap = CodeUtils.createImage(mEntity.getShare_link(), G.WIDTH-40, G.HEIGHT/2, null);
-         iv_qr.setImageBitmap(mBitmap);
+        Glide.with(this)
+                .load(AppUtil.getQRLink(mEntity.getShare_link(), 800))
+                .into(iv_qr);
          dialog.show();
         iv.setOnClickListener(new View.OnClickListener() {
             @Override

+ 0 - 1
app/src/main/java/com/sheep/gamegroup/view/activity/BindDataAct.java

@@ -82,7 +82,6 @@ public class BindDataAct extends BaseActivity implements BindDataContract.View {
 
     @Override
     public void initView() {
-        ButterKnife.bind(this);
         activity = this;
         DaggerBindDataComponent.builder()
                 .netComponent(SheepApp.get(this).getNetComponent())

+ 0 - 7
app/src/main/java/com/sheep/gamegroup/view/activity/BindOrChangeWeixinAct.java

@@ -89,13 +89,6 @@ public class BindOrChangeWeixinAct extends BaseActivity implements BindOrChangeW
 
     }
 
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        // TODO: add setContentView(...) invocation
-        ButterKnife.bind(this);
-    }
-
     @OnClick(R.id.bindweixin_sure_text)
     public void onViewClicked(View view) {
         switch (view.getId()){

+ 0 - 1
app/src/main/java/com/sheep/gamegroup/view/activity/ChangeTelAct.java

@@ -108,7 +108,6 @@ public class ChangeTelAct extends BaseActivity implements PhoneContract.View{
 
     @Override
     public void initView() {
-        ButterKnife.bind(this);
         activity = this;
         DaggerChangePhoneComponent.builder()
                 .netComponent(SheepApp.get(activity).getNetComponent())

+ 0 - 1
app/src/main/java/com/sheep/gamegroup/view/activity/ChangeWxOrTelAct.java

@@ -27,7 +27,6 @@ public class ChangeWxOrTelAct extends BaseActivity {
 
     @Override
     public void initView() {
-        ButterKnife.bind(this);
         TitleBarUtils.getInstance()
                 .setTitle(this,"绑定微信号")
                 .setTitleFinish(this);

+ 0 - 1
app/src/main/java/com/sheep/gamegroup/view/activity/CommitWxAct.java

@@ -72,7 +72,6 @@ public class CommitWxAct extends BaseActivity implements CommitWxMakeMoneyContra
 
     @Override
     public void initView() {
-        ButterKnife.bind(this);
         activity = this;
         TitleBarUtils.getInstance()
                 .setTitle(activity, "挂机赚钱")

+ 0 - 1
app/src/main/java/com/sheep/gamegroup/view/activity/CommitWxCodeAct.java

@@ -112,7 +112,6 @@ public class CommitWxCodeAct extends BaseActivity implements CommitWxCodeMakeMon
 
     @Override
     public void initView() {
-        ButterKnife.bind(this);
         activity = this;
         id = getIntent().getIntExtra("id", 0);
         type = getIntent().getIntExtra("type", 0);

+ 0 - 1
app/src/main/java/com/sheep/gamegroup/view/activity/HomePageAct.java

@@ -87,7 +87,6 @@ public class HomePageAct extends BaseActivity implements HomePageContract.View{
 
     @Override
     public void initView() {
-        ButterKnife.bind(this);
         activity = this;
         DaggerHomePageComponent.builder()
                 .netComponent(SheepApp.get(this).getNetComponent())

+ 0 - 1
app/src/main/java/com/sheep/gamegroup/view/activity/LieMakeMoneyAct.java

@@ -50,7 +50,6 @@ public class LieMakeMoneyAct extends BaseActivity implements LieMakeMoneyContrac
 
     @Override
     public void initView() {
-        ButterKnife.bind(this);
         activity = this;
         TitleBarUtils
                 .getInstance()

+ 8 - 6
app/src/main/java/com/sheep/gamegroup/view/activity/PersionInfoAct.java

@@ -15,12 +15,12 @@ import com.bumptech.glide.request.RequestOptions;
 import com.sheep.gamegroup.event.ImageHeadChange;
 import com.sheep.gamegroup.event.UserNameChange;
 import com.sheep.gamegroup.model.entity.UserInfoEntity;
+import com.sheep.gamegroup.util.AppUtil;
 import com.sheep.gamegroup.util.GlideImageLoader;
 import com.sheep.jiuyan.samllsheep.R;
 import com.sheep.jiuyan.samllsheep.base.AbsChooseImageActivity;
 import com.sheep.jiuyan.samllsheep.utils.G;
 import com.sheep.jiuyan.samllsheep.utils.TitleBarUtils;
-import com.uuzuche.lib_zxing.activity.CodeUtils;
 
 import org.greenrobot.eventbus.EventBus;
 import org.greenrobot.eventbus.Subscribe;
@@ -29,7 +29,6 @@ import org.greenrobot.eventbus.ThreadMode;
 import butterknife.BindView;
 import butterknife.ButterKnife;
 import butterknife.OnClick;
-import de.hdodenhof.circleimageview.CircleImageView;
 
 
 /**
@@ -85,8 +84,10 @@ public class PersionInfoAct extends AbsChooseImageActivity {
             if (mEntity != null) {
                 tvNike.setText(mEntity.getNickname());
                 tvSheepId.setText(mEntity.getInvitation_code());
-                Bitmap mBitmap = CodeUtils.createImage(mEntity.getShare_link(), 30, 30, null);
-                tvQr.setImageBitmap(mBitmap);
+
+                Glide.with(PersionInfoAct.this)
+                        .load(AppUtil.getQRLink(mEntity.getShare_link(), 90))
+                        .into(tvQr);
 
                 Glide.with(PersionInfoAct.this)
                         .load(mEntity.getAvatar())
@@ -117,8 +118,9 @@ public class PersionInfoAct extends AbsChooseImageActivity {
         final AlertDialog dialog = builder.create();
         ImageView iv = view.findViewById(R.id.iv_close);
         ImageView iv_qr = view.findViewById(R.id.iv_qr);
-        Bitmap mBitmap = CodeUtils.createImage(mEntity.getShare_link(), G.WIDTH - 40, G.HEIGHT / 2, null);
-        iv_qr.setImageBitmap(mBitmap);
+        Glide.with(PersionInfoAct.this)
+                .load(AppUtil.getQRLink(mEntity.getShare_link(), 800))
+                .into(iv_qr);
         dialog.show();
         iv.setOnClickListener(new View.OnClickListener() {
             @Override

+ 0 - 2
app/src/main/java/com/sheep/gamegroup/view/activity/PersonalCenterAct.java

@@ -78,8 +78,6 @@ public class PersonalCenterAct extends BaseActivity implements PersonalCenterCon
 
     @Override
     public void initView() {
-
-        ButterKnife.bind(this);
         activity = this;
         DaggerPersonalCenterComponent.builder()
                 .netComponent(SheepApp.get(this).getNetComponent())

+ 0 - 9
app/src/main/java/com/sheep/gamegroup/view/activity/PhoneAct.java

@@ -141,15 +141,6 @@ public class PhoneAct extends BaseActivity implements PhoneContract.View {
 
     }
 
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        // TODO: add setContentView(...) invocation
-        ButterKnife.bind(this);
-//        phoneEtAccount.setText("17628083502");
-//        phoneEtCode.setText("123456");
-    }
-
     @OnClick({R.id.phone_et_account, R.id.phone_et_code, R.id.phone_btn_code, R.id.phone_sure_tv})
     public void onViewClicked(View view) {
         switch (view.getId()) {

+ 4 - 8
app/src/main/java/com/sheep/gamegroup/view/activity/ReNameActivity.java

@@ -86,16 +86,12 @@ public class ReNameActivity extends BaseActivity implements ReNameContract.View
     @Override
     public void initData() {
         Intent i = getIntent();
-        if (i != null && i.hasExtra("name")) {
-            et.setText(i.getStringExtra("name"));
+        String name;
+        if (i != null && i.hasExtra("name") && !android.text.TextUtils.isEmpty(name = i.getStringExtra("name"))) {
+            et.setText(name);
+            et.setSelection(name.length());
         }
 
-
-        TitleBarUtils
-                .getInstance()
-                .setTitle(this, "修改昵称")
-                .setTitleFinish(this);
-
     }
 
 

+ 0 - 3
app/src/main/java/com/sheep/gamegroup/view/activity/RealNameAuthenAct.java

@@ -18,13 +18,11 @@ import com.sheep.gamegroup.model.entity.UserInfoEntity;
 import com.sheep.gamegroup.presenter.RealNameAutherContract;
 import com.sheep.gamegroup.presenter.RealNameAutherPresenter;
 import com.sheep.gamegroup.util.StringUtils;
-import com.sheep.gamegroup.view.dialog.DialogAccountAbnormal;
 import com.sheep.gamegroup.view.dialog.DialogRealNameAuther;
 import com.sheep.jiuyan.samllsheep.R;
 
 import com.sheep.jiuyan.samllsheep.SheepApp;
 import com.sheep.jiuyan.samllsheep.utils.TitleBarUtils;
-import com.uuzuche.lib_zxing.activity.CodeUtils;
 
 import org.greenrobot.eventbus.EventBus;
 
@@ -33,7 +31,6 @@ import java.util.HashMap;
 import javax.inject.Inject;
 
 import butterknife.BindView;
-import butterknife.ButterKnife;
 import butterknife.OnClick;
 
 /**

+ 0 - 7
app/src/main/java/com/sheep/gamegroup/view/activity/SplashAct.java

@@ -213,13 +213,6 @@ public class SplashAct extends BaseActivity implements SplashContract.View, Easy
         super.onActivityResult(requestCode, resultCode, data);
     }
 
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        // TODO: add setContentView(...) invocation
-        ButterKnife.bind(this);
-    }
-
     @OnClick(R.id.btn_join)
     public void onViewClicked(View view) {
         switch (view.getId()){

+ 0 - 1
app/src/main/java/com/sheep/gamegroup/view/activity/TaskListAct.java

@@ -54,7 +54,6 @@ public class TaskListAct extends BaseActivity implements TaskListContract.View{
 
     @Override
     public void initView() {
-        ButterKnife.bind(this);
         activity = this;
         DaggerTaskListComponent.builder()
                 .netComponent(SheepApp.get(this).getNetComponent())

+ 0 - 1
app/src/main/java/com/sheep/gamegroup/view/activity/TryMakeMoneyact.java

@@ -74,7 +74,6 @@ public class TryMakeMoneyact extends BaseActivity implements TryMakeMoneyContrac
     @Override
     public void initView() {
         EventBus.getDefault().register(this);
-        ButterKnife.bind(this);
         activity = this;
         TitleBarUtils
                 .getInstance()

+ 0 - 1
app/src/main/java/com/sheep/gamegroup/view/activity/WithdrawalAct.java

@@ -71,7 +71,6 @@ public class WithdrawalAct extends BaseActivity implements WithdrawalContract.Vi
 
     @Override
     public void initView() {
-        ButterKnife.bind(this);
         activity = this;
         TitleBarUtils
                 .getInstance()

+ 0 - 1
app/src/main/java/com/sheep/gamegroup/view/activity/WithdrawalListAct.java

@@ -50,7 +50,6 @@ public class WithdrawalListAct extends BaseActivity implements WithdrawalListCon
 
     @Override
     public void initView() {
-        ButterKnife.bind(this);
         activity = this;
         DaggerWithdrawalListComponent.builder()
                 .netComponent(SheepApp.get(this).getNetComponent())

+ 0 - 7
app/src/main/java/com/sheep/gamegroup/view/activity/WithdrawalResultAct.java

@@ -78,13 +78,6 @@ public class WithdrawalResultAct extends BaseActivity implements WithdrawalResul
 //        withdrawalSuccessMoneyTv.setText("+"+amount+"元");
     }
 
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        // TODO: add setContentView(...) invocation
-        ButterKnife.bind(this);
-    }
-
     @OnClick({R.id.withdrawal_success_sure_tv})
     public void onViewClicked(View view) {
         switch (view.getId()) {

+ 2 - 3
app/src/main/java/com/sheep/jiuyan/samllsheep/BaseApplication.java

@@ -5,7 +5,6 @@ import com.kfzs.duanduan.KFZSApp;
 import com.umeng.commonsdk.UMConfigure;
 import com.umeng.socialize.PlatformConfig;
 import com.umeng.socialize.UMShareAPI;
-import com.uuzuche.lib_zxing.activity.ZXingLibrary;
 
 import org.xutils.BuildConfig;
 import org.xutils.x;
@@ -14,14 +13,14 @@ import org.xutils.x;
  * Created by Administrator on 2018/1/19.
  */
 
-public class BaseApplication extends KFZSApp {
+public abstract class BaseApplication extends KFZSApp {
 
     @Override
     public void onCreate() {
         super.onCreate();
         x.Ext.init(this);
         x.Ext.setDebug(BuildConfig.DEBUG); // 是否输出debug日志, 开启debug会影响性能.
-        ZXingLibrary.initDisplayOpinion(this);
+//        ZXingLibrary.initDisplayOpinion(this);
         UMConfigure.setLogEnabled(true);
         UMConfigure.init(this, UMConfigure.DEVICE_TYPE_PHONE, "5ab0a1da8f4a9d742900035f");
         UMShareAPI.get(this);

+ 50 - 11
app/src/main/java/com/sheep/jiuyan/samllsheep/page/AskFriendActivity.java

@@ -19,6 +19,9 @@ import android.widget.RelativeLayout;
 import android.widget.TextView;
 import android.widget.Toast;
 
+import com.bumptech.glide.Glide;
+import com.orhanobut.logger.Logger;
+import com.sheep.gamegroup.util.AppUtil;
 import com.sheep.jiuyan.samllsheep.R;
 import com.sheep.jiuyan.samllsheep.base.BaseActivity;
 import com.sheep.jiuyan.samllsheep.net.NetManager;
@@ -26,17 +29,18 @@ import com.sheep.jiuyan.samllsheep.net.SheepCallback;
 import com.sheep.jiuyan.samllsheep.net.Url;
 import com.sheep.jiuyan.samllsheep.page.entry.InviteUrl;
 import com.sheep.jiuyan.samllsheep.page.entry.UerInviteInfo;
-import com.sheep.jiuyan.samllsheep.utils.DeviceUtils;
-import com.sheep.jiuyan.samllsheep.utils.PackageUtil;
 import com.sheep.jiuyan.samllsheep.utils.SpUtils;
 import com.sheep.jiuyan.samllsheep.utils.TitleBarUtils;
-import com.uuzuche.lib_zxing.activity.CodeUtils;
+import com.zhy.http.okhttp.OkHttpUtils;
+import com.zhy.http.okhttp.callback.FileCallBack;
 
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.HashMap;
 
+import okhttp3.Call;
+
 /**
  * Created by Administrator on 2018/1/24.
  */
@@ -167,23 +171,58 @@ public class AskFriendActivity extends BaseActivity implements View.OnClickListe
         });
 
         ImageView iv_ema = (ImageView) view.findViewById(R.id.iv_ema);
-        final Bitmap mBitmap = CodeUtils.createImage(shareLink, 200, 200, null);
-        iv_ema.setImageBitmap(mBitmap);
+        Glide.with(this)
+                .load(AppUtil.getQRLink(shareLink, 800))
+                .into(iv_ema);
         LinearLayout ll_save = (LinearLayout) view.findViewById(R.id.ll_save);
         ll_save.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View view) {
-                boolean isOk = saveImageToGallery(AskFriendActivity.this, mBitmap);
-                if (isOk) {
-                    Toast.makeText(AskFriendActivity.this, "保存成功", Toast.LENGTH_SHORT).show();
-                } else {
-                    Toast.makeText(AskFriendActivity.this, "保存失败,请重试", Toast.LENGTH_SHORT).show();
-                }
+//                boolean isOk = saveImageToGallery(AskFriendActivity.this, mBitmap);
+//                if (isOk) {
+//                    Toast.makeText(AskFriendActivity.this, "保存成功", Toast.LENGTH_SHORT).show();
+//                } else {
+//                    Toast.makeText(AskFriendActivity.this, "保存失败,请重试", Toast.LENGTH_SHORT).show();
+//                }
+                downLoadImg(AppUtil.getQRLink(shareLink, 800));
             }
         });
     }
 
     //保存图片,更新系统图库
+    public boolean downLoadImg(String url) {
+        String storePath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "Sheep";
+        File appDir = new File(storePath);
+        if (!appDir.exists()) {
+            appDir.mkdirs();
+        }
+        String fileName = System.currentTimeMillis() + ".jpg";
+        OkHttpUtils.get()
+                .url(url)
+                .build()
+                .execute(new FileCallBack(appDir.getAbsolutePath(), fileName) {
+                    @Override
+                    public void onError(Call call, Exception e, int id) {
+                        Logger.e("onError :" + e.getMessage());
+                        Toast.makeText(AskFriendActivity.this, "保存失败,请重试", Toast.LENGTH_SHORT).show();
+                    }
+
+                    @Override
+                    public void inProgress(float progress, long total, int id) {
+                        //super.inProgress(progress, total, id);
+                        Logger.e("inProgress"+(int)(100*progress));
+                    }
+
+                    @Override
+                    public void onResponse(File file, int id) {
+                        Logger.e("onResponse :" + file.getAbsolutePath());
+                        Toast.makeText(AskFriendActivity.this, "保存成功", Toast.LENGTH_SHORT).show();
+                    }
+                });
+        return true;
+    }
+
+    //保存图片,更新系统图库
     public boolean saveImageToGallery(Context context, Bitmap bmp) {
         String storePath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "Sheep";
         File appDir = new File(storePath);

+ 2 - 4
app/src/main/res/layout/x_ask_qr_dialog.xml

@@ -2,16 +2,14 @@
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:layout_marginLeft="20dp"
-    android:layout_marginRight="20dp"
-    android:layout_marginBottom="20dp"
+    android:background="#ffffff"
     android:orientation="vertical">
 
     <ImageView
         android:id="@+id/iv_close"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:src="@drawable/icon_close_diaolog"
+        android:src="@mipmap/try_dialog_x"
         android:layout_gravity="right"
         />
 

+ 4 - 12
app/src/main/res/layout/x_ask_reward_dialog.xml

@@ -8,7 +8,7 @@
         android:id="@+id/iv_close"
         android:layout_width="30dp"
         android:layout_height="30dp"
-        android:src="@drawable/icon_close_diaolog"
+        android:src="@mipmap/try_dialog_x"
         android:layout_gravity="right"
         />
 
@@ -44,20 +44,12 @@
 
         <TextView
             android:id="@+id/withdrawal"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:minWidth="160dp"
-            android:layout_alignParentRight="true"
-            android:layout_centerVertical="true"
+            style="@style/style_button"
             android:text="复制链接"
-            android:textColor="@color/black_444444"
-            android:textSize="18dp"
-            android:background="@drawable/selector_button_full_main"
-            android:gravity="center"
-            android:layout_gravity="center"
             android:layout_marginTop="20dp"
             android:layout_marginBottom="20dp"
-            android:padding="7dp"/>
+            android:layout_marginLeft="100dp"
+            android:layout_marginRight="100dp"/>
 
     </LinearLayout>
 </LinearLayout>

+ 2 - 13
app/src/main/res/layout/x_rename_act_layout.xml

@@ -26,19 +26,8 @@
 
     <TextView
         android:id="@+id/tv_submit"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center_horizontal"
-
-        android:layout_marginTop="30dp"
-        android:layout_marginLeft="20dp"
-        android:layout_marginRight="20dp"
-        android:background="@drawable/selector_button_full_main"
-        android:gravity="center"
-        android:padding="@dimen/content_padding_10"
-        android:text="确定"
-        android:textColor="@color/white_DEDEDE"
-        android:textSize="@dimen/text_size_18" />
+        style="@style/style_button"
+        android:text="确定" />
 
 
 </LinearLayout>

+ 9 - 4
app/src/main/res/layout/xpersion_info_act_layout.xml

@@ -12,9 +12,12 @@
         android:id="@+id/rl_head"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:padding="15dp"
-        android:background="@drawable/x_shap_shadow_bg_rectgangle_white"
         android:layout_margin="10dp"
+        android:background="@drawable/x_shap_shadow_bg_rectgangle_white"
+        android:paddingTop="10dp"
+        android:paddingBottom="10dp"
+        android:paddingLeft="25dp"
+        android:paddingRight="25dp"
         >
 
         <TextView
@@ -133,7 +136,7 @@
 
 
 
-            <TextView
+            <EditText
                 android:id="@+id/tv_sheep_id"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
@@ -141,7 +144,9 @@
                 android:src="@mipmap/ic_launcher"
                 android:layout_alignParentEnd="true"
                 android:layout_centerVertical="true"
-
+                android:textCursorDrawable="@null"
+                android:inputType="none"
+                android:textIsSelectable="true"
                 />
 
         </RelativeLayout>

+ 3 - 0
build.gradle

@@ -22,6 +22,9 @@ buildscript {
     }
 }
 
+def isReleaseBuild() {
+    return version.contains("SNAPSHOT") == false
+}
 allprojects {
     repositories {
         maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'}

+ 0 - 3
datashare/build.gradle

@@ -11,9 +11,6 @@ android {
         versionName VERSION_NAME
     }
 
-    dexOptions {
-        incremental true
-    }
     greendao {
         schemaVersion 2
         targetGenDir 'src/main/java'

+ 12 - 1
gradle.properties

@@ -22,4 +22,15 @@ VERSION_CODE=200000
 ANDROID_COMPILE_SDK_VERSION=27
 ANDROID_BUILD_TOOLS_VERSION=27.0.2
 ANDROID_MIN_SDK_VERSION=18
-ANDORID_TARGET_SDK_VERSION=27
+ANDORID_TARGET_SDK_VERSION=27
+
+POM_DESCRIPTION=Android Library for cropping images
+POM_URL=https://github.com/Yalantis/uCrop
+POM_SCM_URL=https://github.com/Yalantis/uCrop
+POM_SCM_CONNECTION=scm:git@github.com/Yalantis/uCrop.git
+POM_SCM_DEV_CONNECTION=scm:git@github.com/Yalantis/uCrop.git
+POM_LICENCE_NAME=The Apache Software License, Version 2.0
+POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0
+POM_LICENCE_DIST=repo
+POM_DEVELOPER_ID=yalantis
+POM_DEVELOPER_NAME=Yalantis

+ 91 - 0
mavenpush.gradle

@@ -0,0 +1,91 @@
+apply plugin: 'maven'
+apply plugin: 'signing'
+
+def sonatypeRepositoryUrl
+if (isReleaseBuild()) {
+    println 'RELEASE BUILD'
+    sonatypeRepositoryUrl = hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL
+            : "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
+} else {
+    println 'DEBUG BUILD'
+    sonatypeRepositoryUrl = hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL
+            : "https://oss.sonatype.org/content/repositories/snapshots/"
+}
+def getRepositoryUsername() {
+    return hasProperty('nexusUsername') ? nexusUsername : ""
+}
+
+def getRepositoryPassword() {
+    return hasProperty('nexusPassword') ? nexusPassword : ""
+}
+
+afterEvaluate { project ->
+    uploadArchives {
+        repositories {
+            mavenDeployer {
+                beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
+
+                pom.artifactId = POM_ARTIFACT_ID
+
+                repository(url: sonatypeRepositoryUrl) {
+                    authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
+                }
+
+                pom.project {
+                    name POM_NAME
+                    packaging POM_PACKAGING
+                    description POM_DESCRIPTION
+                    url POM_URL
+
+                    scm {
+                        url POM_SCM_URL
+                        connection POM_SCM_CONNECTION
+                        developerConnection POM_SCM_DEV_CONNECTION
+                    }
+
+                    licenses {
+                        license {
+                            name POM_LICENCE_NAME
+                            url POM_LICENCE_URL
+                            distribution POM_LICENCE_DIST
+                        }
+                    }
+
+                    developers {
+                        developer {
+                            id POM_DEVELOPER_ID
+                            name POM_DEVELOPER_NAME
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    signing {
+        required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
+        sign configurations.archives
+    }
+
+    task androidJavadocs(type: Javadoc) {
+        source = android.sourceSets.main.java.sourceFiles
+    }
+
+    task androidJavadocsJar(type: Jar) {
+        classifier = 'javadoc'
+        //basename = artifact_id
+        from androidJavadocs.destinationDir
+    }
+
+    task androidSourcesJar(type: Jar) {
+        classifier = 'sources'
+        //basename = artifact_id
+        from android.sourceSets.main.java.sourceFiles
+    }
+
+    artifacts {
+        //archives packageReleaseJar
+        archives androidSourcesJar
+        archives androidJavadocsJar
+    }
+}

+ 1 - 1
settings.gradle

@@ -1 +1 @@
-include ':app', ':datashare', ':view', ':kfzslibrary'
+include ':app', ':datashare', ':view', ':kfzslibrary', ':ucrop'

+ 1 - 0
ucrop/.gitignore

@@ -0,0 +1 @@
+/build

+ 41 - 0
ucrop/build.gradle

@@ -0,0 +1,41 @@
+apply plugin: 'com.android.library'
+apply from: '../mavenpush.gradle'
+
+android {
+    compileSdkVersion 25
+    buildToolsVersion '25.0.3'
+
+    defaultConfig {
+        minSdkVersion 14
+        targetSdkVersion 25
+        versionCode 22
+        versionName "2.2.0-native"
+
+        vectorDrawables.useSupportLibrary = true
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_7
+        targetCompatibility JavaVersion.VERSION_1_7
+    }
+    lintOptions {
+        abortOnError false
+    }
+
+    resourcePrefix 'ucrop_'
+
+    sourceSets.main {
+        jni.srcDirs = [];
+    }
+
+}
+
+dependencies {
+    compile 'com.android.support:appcompat-v7:25.3.1'
+    compile 'com.squareup.okhttp3:okhttp:3.8.1'
+}

+ 3 - 0
ucrop/gradle.properties

@@ -0,0 +1,3 @@
+POM_NAME=uCrop
+POM_ARTIFACT_ID=ucrop
+POM_PACKAGING=aar

+ 17 - 0
ucrop/proguard-rules.pro

@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/oleksii/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}

+ 2 - 0
ucrop/src/main/AndroidManifest.xml

@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest package="com.yalantis.ucrop"/>

+ 538 - 0
ucrop/src/main/java/com/yalantis/ucrop/UCrop.java

@@ -0,0 +1,538 @@
+package com.yalantis.ucrop;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.annotation.ColorInt;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.FloatRange;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.yalantis.ucrop.model.AspectRatio;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ * <p/>
+ * Builder class to ease Intent setup.
+ */
+public class UCrop {
+
+    public static final int REQUEST_CROP = 69;
+    public static final int RESULT_ERROR = 96;
+
+    private static final String EXTRA_PREFIX = BuildConfig.APPLICATION_ID;
+
+    public static final String EXTRA_INPUT_URI = EXTRA_PREFIX + ".InputUri";
+    public static final String EXTRA_OUTPUT_URI = EXTRA_PREFIX + ".OutputUri";
+    public static final String EXTRA_OUTPUT_CROP_ASPECT_RATIO = EXTRA_PREFIX + ".CropAspectRatio";
+    public static final String EXTRA_OUTPUT_IMAGE_WIDTH = EXTRA_PREFIX + ".ImageWidth";
+    public static final String EXTRA_OUTPUT_IMAGE_HEIGHT = EXTRA_PREFIX + ".ImageHeight";
+    public static final String EXTRA_OUTPUT_OFFSET_X = EXTRA_PREFIX + ".OffsetX";
+    public static final String EXTRA_OUTPUT_OFFSET_Y = EXTRA_PREFIX + ".OffsetY";
+    public static final String EXTRA_ERROR = EXTRA_PREFIX + ".Error";
+
+    public static final String EXTRA_ASPECT_RATIO_X = EXTRA_PREFIX + ".AspectRatioX";
+    public static final String EXTRA_ASPECT_RATIO_Y = EXTRA_PREFIX + ".AspectRatioY";
+
+    public static final String EXTRA_MAX_SIZE_X = EXTRA_PREFIX + ".MaxSizeX";
+    public static final String EXTRA_MAX_SIZE_Y = EXTRA_PREFIX + ".MaxSizeY";
+
+    private Intent mCropIntent;
+    private Bundle mCropOptionsBundle;
+
+    /**
+     * This method creates new Intent builder and sets both source and destination image URIs.
+     *
+     * @param source      Uri for image to crop
+     * @param destination Uri for saving the cropped image
+     */
+    public static UCrop of(@NonNull Uri source, @NonNull Uri destination) {
+        return new UCrop(source, destination);
+    }
+
+    private UCrop(@NonNull Uri source, @NonNull Uri destination) {
+        mCropIntent = new Intent();
+        mCropOptionsBundle = new Bundle();
+        mCropOptionsBundle.putParcelable(EXTRA_INPUT_URI, source);
+        mCropOptionsBundle.putParcelable(EXTRA_OUTPUT_URI, destination);
+    }
+
+    /**
+     * Set an aspect ratio for crop bounds.
+     * User won't see the menu with other ratios options.
+     *
+     * @param x aspect ratio X
+     * @param y aspect ratio Y
+     */
+    public UCrop withAspectRatio(float x, float y) {
+        mCropOptionsBundle.putFloat(EXTRA_ASPECT_RATIO_X, x);
+        mCropOptionsBundle.putFloat(EXTRA_ASPECT_RATIO_Y, y);
+        return this;
+    }
+
+    /**
+     * Set an aspect ratio for crop bounds that is evaluated from source image width and height.
+     * User won't see the menu with other ratios options.
+     */
+    public UCrop useSourceImageAspectRatio() {
+        mCropOptionsBundle.putFloat(EXTRA_ASPECT_RATIO_X, 0);
+        mCropOptionsBundle.putFloat(EXTRA_ASPECT_RATIO_Y, 0);
+        return this;
+    }
+
+    /**
+     * Set maximum size for result cropped image.
+     *
+     * @param width  max cropped image width
+     * @param height max cropped image height
+     */
+    public UCrop withMaxResultSize(@IntRange(from = 100) int width, @IntRange(from = 100) int height) {
+        mCropOptionsBundle.putInt(EXTRA_MAX_SIZE_X, width);
+        mCropOptionsBundle.putInt(EXTRA_MAX_SIZE_Y, height);
+        return this;
+    }
+
+    public UCrop withOptions(@NonNull Options options) {
+        mCropOptionsBundle.putAll(options.getOptionBundle());
+        return this;
+    }
+
+    /**
+     * Send the crop Intent from an Activity
+     *
+     * @param activity Activity to receive result
+     */
+    public void start(@NonNull Activity activity) {
+        start(activity, REQUEST_CROP);
+    }
+
+    /**
+     * Send the crop Intent from an Activity with a custom request code
+     *
+     * @param activity    Activity to receive result
+     * @param requestCode requestCode for result
+     */
+    public void start(@NonNull Activity activity, int requestCode) {
+        activity.startActivityForResult(getIntent(activity), requestCode);
+    }
+
+    /**
+     * Send the crop Intent from a Fragment
+     *
+     * @param fragment Fragment to receive result
+     */
+    public void start(@NonNull Context context, @NonNull Fragment fragment) {
+        start(context, fragment, REQUEST_CROP);
+    }
+
+    /**
+     * Send the crop Intent from a support library Fragment
+     *
+     * @param fragment Fragment to receive result
+     */
+    public void start(@NonNull Context context, @NonNull android.support.v4.app.Fragment fragment) {
+        start(context, fragment, REQUEST_CROP);
+    }
+
+    /**
+     * Send the crop Intent with a custom request code
+     *
+     * @param fragment    Fragment to receive result
+     * @param requestCode requestCode for result
+     */
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    public void start(@NonNull Context context, @NonNull Fragment fragment, int requestCode) {
+        fragment.startActivityForResult(getIntent(context), requestCode);
+    }
+
+    /**
+     * Send the crop Intent with a custom request code
+     *
+     * @param fragment    Fragment to receive result
+     * @param requestCode requestCode for result
+     */
+    public void start(@NonNull Context context, @NonNull android.support.v4.app.Fragment fragment, int requestCode) {
+        fragment.startActivityForResult(getIntent(context), requestCode);
+    }
+
+    /**
+     * Get Intent to start {@link UCropActivity}
+     *
+     * @return Intent for {@link UCropActivity}
+     */
+    public Intent getIntent(@NonNull Context context) {
+        mCropIntent.setClass(context, UCropActivity.class);
+        mCropIntent.putExtras(mCropOptionsBundle);
+        return mCropIntent;
+    }
+
+    /**
+     * Retrieve cropped image Uri from the result Intent
+     *
+     * @param intent crop result intent
+     */
+    @Nullable
+    public static Uri getOutput(@NonNull Intent intent) {
+        return intent.getParcelableExtra(EXTRA_OUTPUT_URI);
+    }
+
+    /**
+     * Retrieve the width of the cropped image
+     *
+     * @param intent crop result intent
+     */
+    public static int getOutputImageWidth(@NonNull Intent intent) {
+        return intent.getIntExtra(EXTRA_OUTPUT_IMAGE_WIDTH, -1);
+    }
+
+    /**
+     * Retrieve the height of the cropped image
+     *
+     * @param intent crop result intent
+     */
+    public static int getOutputImageHeight(@NonNull Intent intent) {
+        return intent.getIntExtra(EXTRA_OUTPUT_IMAGE_HEIGHT, -1);
+    }
+
+    /**
+     * Retrieve cropped image aspect ratio from the result Intent
+     *
+     * @param intent crop result intent
+     * @return aspect ratio as a floating point value (x:y) - so it will be 1 for 1:1 or 4/3 for 4:3
+     */
+    public static float getOutputCropAspectRatio(@NonNull Intent intent) {
+        return intent.getParcelableExtra(EXTRA_OUTPUT_CROP_ASPECT_RATIO);
+    }
+
+    /**
+     * Method retrieves error from the result intent.
+     *
+     * @param result crop result Intent
+     * @return Throwable that could happen while image processing
+     */
+    @Nullable
+    public static Throwable getError(@NonNull Intent result) {
+        return (Throwable) result.getSerializableExtra(EXTRA_ERROR);
+    }
+
+
+    /**
+     * Class that helps to setup advanced configs that are not commonly used.
+     * Use it with method {@link #withOptions(Options)}
+     */
+    public static class Options {
+
+        public static final String EXTRA_COMPRESSION_FORMAT_NAME = EXTRA_PREFIX + ".CompressionFormatName";
+        public static final String EXTRA_COMPRESSION_QUALITY = EXTRA_PREFIX + ".CompressionQuality";
+
+        public static final String EXTRA_ALLOWED_GESTURES = EXTRA_PREFIX + ".AllowedGestures";
+
+        public static final String EXTRA_MAX_BITMAP_SIZE = EXTRA_PREFIX + ".MaxBitmapSize";
+        public static final String EXTRA_MAX_SCALE_MULTIPLIER = EXTRA_PREFIX + ".MaxScaleMultiplier";
+        public static final String EXTRA_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION = EXTRA_PREFIX + ".ImageToCropBoundsAnimDuration";
+
+        public static final String EXTRA_DIMMED_LAYER_COLOR = EXTRA_PREFIX + ".DimmedLayerColor";
+        public static final String EXTRA_CIRCLE_DIMMED_LAYER = EXTRA_PREFIX + ".CircleDimmedLayer";
+
+        public static final String EXTRA_SHOW_CROP_FRAME = EXTRA_PREFIX + ".ShowCropFrame";
+        public static final String EXTRA_CROP_FRAME_COLOR = EXTRA_PREFIX + ".CropFrameColor";
+        public static final String EXTRA_CROP_FRAME_STROKE_WIDTH = EXTRA_PREFIX + ".CropFrameStrokeWidth";
+
+        public static final String EXTRA_SHOW_CROP_GRID = EXTRA_PREFIX + ".ShowCropGrid";
+        public static final String EXTRA_CROP_GRID_ROW_COUNT = EXTRA_PREFIX + ".CropGridRowCount";
+        public static final String EXTRA_CROP_GRID_COLUMN_COUNT = EXTRA_PREFIX + ".CropGridColumnCount";
+        public static final String EXTRA_CROP_GRID_COLOR = EXTRA_PREFIX + ".CropGridColor";
+        public static final String EXTRA_CROP_GRID_STROKE_WIDTH = EXTRA_PREFIX + ".CropGridStrokeWidth";
+
+        public static final String EXTRA_TOOL_BAR_COLOR = EXTRA_PREFIX + ".ToolbarColor";
+        public static final String EXTRA_STATUS_BAR_COLOR = EXTRA_PREFIX + ".StatusBarColor";
+        public static final String EXTRA_UCROP_COLOR_WIDGET_ACTIVE = EXTRA_PREFIX + ".UcropColorWidgetActive";
+
+        public static final String EXTRA_UCROP_WIDGET_COLOR_TOOLBAR = EXTRA_PREFIX + ".UcropToolbarWidgetColor";
+        public static final String EXTRA_UCROP_TITLE_TEXT_TOOLBAR = EXTRA_PREFIX + ".UcropToolbarTitleText";
+        public static final String EXTRA_UCROP_WIDGET_CANCEL_DRAWABLE = EXTRA_PREFIX + ".UcropToolbarCancelDrawable";
+        public static final String EXTRA_UCROP_WIDGET_CROP_DRAWABLE = EXTRA_PREFIX + ".UcropToolbarCropDrawable";
+
+        public static final String EXTRA_UCROP_LOGO_COLOR = EXTRA_PREFIX + ".UcropLogoColor";
+
+        public static final String EXTRA_HIDE_BOTTOM_CONTROLS = EXTRA_PREFIX + ".HideBottomControls";
+        public static final String EXTRA_FREE_STYLE_CROP = EXTRA_PREFIX + ".FreeStyleCrop";
+
+        public static final String EXTRA_ASPECT_RATIO_SELECTED_BY_DEFAULT = EXTRA_PREFIX + ".AspectRatioSelectedByDefault";
+        public static final String EXTRA_ASPECT_RATIO_OPTIONS = EXTRA_PREFIX + ".AspectRatioOptions";
+
+        public static final String EXTRA_UCROP_ROOT_VIEW_BACKGROUND_COLOR = EXTRA_PREFIX + ".UcropRootViewBackgroundColor";
+
+
+        private final Bundle mOptionBundle;
+
+        public Options() {
+            mOptionBundle = new Bundle();
+        }
+
+        @NonNull
+        public Bundle getOptionBundle() {
+            return mOptionBundle;
+        }
+
+        /**
+         * Set one of {@link android.graphics.Bitmap.CompressFormat} that will be used to save resulting Bitmap.
+         */
+        public void setCompressionFormat(@NonNull Bitmap.CompressFormat format) {
+            mOptionBundle.putString(EXTRA_COMPRESSION_FORMAT_NAME, format.name());
+        }
+
+        /**
+         * Set compression quality [0-100] that will be used to save resulting Bitmap.
+         */
+        public void setCompressionQuality(@IntRange(from = 0) int compressQuality) {
+            mOptionBundle.putInt(EXTRA_COMPRESSION_QUALITY, compressQuality);
+        }
+
+        /**
+         * Choose what set of gestures will be enabled on each tab - if any.
+         */
+        public void setAllowedGestures(@UCropActivity.GestureTypes int tabScale,
+                                       @UCropActivity.GestureTypes int tabRotate,
+                                       @UCropActivity.GestureTypes int tabAspectRatio) {
+            mOptionBundle.putIntArray(EXTRA_ALLOWED_GESTURES, new int[]{tabScale, tabRotate, tabAspectRatio});
+        }
+
+        /**
+         * This method sets multiplier that is used to calculate max image scale from min image scale.
+         *
+         * @param maxScaleMultiplier - (minScale * maxScaleMultiplier) = maxScale
+         */
+        public void setMaxScaleMultiplier(@FloatRange(from = 1.0, fromInclusive = false) float maxScaleMultiplier) {
+            mOptionBundle.putFloat(EXTRA_MAX_SCALE_MULTIPLIER, maxScaleMultiplier);
+        }
+
+        /**
+         * This method sets animation duration for image to wrap the crop bounds
+         *
+         * @param durationMillis - duration in milliseconds
+         */
+        public void setImageToCropBoundsAnimDuration(@IntRange(from = 100) int durationMillis) {
+            mOptionBundle.putInt(EXTRA_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION, durationMillis);
+        }
+
+        /**
+         * Setter for max size for both width and height of bitmap that will be decoded from an input Uri and used in the view.
+         *
+         * @param maxBitmapSize - size in pixels
+         */
+        public void setMaxBitmapSize(@IntRange(from = 100) int maxBitmapSize) {
+            mOptionBundle.putInt(EXTRA_MAX_BITMAP_SIZE, maxBitmapSize);
+        }
+
+        /**
+         * @param color - desired color of dimmed area around the crop bounds
+         */
+        public void setDimmedLayerColor(@ColorInt int color) {
+            mOptionBundle.putInt(EXTRA_DIMMED_LAYER_COLOR, color);
+        }
+
+        /**
+         * @param isCircle - set it to true if you want dimmed layer to have an circle inside
+         */
+        public void setCircleDimmedLayer(boolean isCircle) {
+            mOptionBundle.putBoolean(EXTRA_CIRCLE_DIMMED_LAYER, isCircle);
+        }
+
+        /**
+         * @param show - set to true if you want to see a crop frame rectangle on top of an image
+         */
+        public void setShowCropFrame(boolean show) {
+            mOptionBundle.putBoolean(EXTRA_SHOW_CROP_FRAME, show);
+        }
+
+        /**
+         * @param color - desired color of crop frame
+         */
+        public void setCropFrameColor(@ColorInt int color) {
+            mOptionBundle.putInt(EXTRA_CROP_FRAME_COLOR, color);
+        }
+
+        /**
+         * @param width - desired width of crop frame line in pixels
+         */
+        public void setCropFrameStrokeWidth(@IntRange(from = 0) int width) {
+            mOptionBundle.putInt(EXTRA_CROP_FRAME_STROKE_WIDTH, width);
+        }
+
+        /**
+         * @param show - set to true if you want to see a crop grid/guidelines on top of an image
+         */
+        public void setShowCropGrid(boolean show) {
+            mOptionBundle.putBoolean(EXTRA_SHOW_CROP_GRID, show);
+        }
+
+        /**
+         * @param count - crop grid rows count.
+         */
+        public void setCropGridRowCount(@IntRange(from = 0) int count) {
+            mOptionBundle.putInt(EXTRA_CROP_GRID_ROW_COUNT, count);
+        }
+
+        /**
+         * @param count - crop grid columns count.
+         */
+        public void setCropGridColumnCount(@IntRange(from = 0) int count) {
+            mOptionBundle.putInt(EXTRA_CROP_GRID_COLUMN_COUNT, count);
+        }
+
+        /**
+         * @param color - desired color of crop grid/guidelines
+         */
+        public void setCropGridColor(@ColorInt int color) {
+            mOptionBundle.putInt(EXTRA_CROP_GRID_COLOR, color);
+        }
+
+        /**
+         * @param width - desired width of crop grid lines in pixels
+         */
+        public void setCropGridStrokeWidth(@IntRange(from = 0) int width) {
+            mOptionBundle.putInt(EXTRA_CROP_GRID_STROKE_WIDTH, width);
+        }
+
+        /**
+         * @param color - desired resolved color of the toolbar
+         */
+        public void setToolbarColor(@ColorInt int color) {
+            mOptionBundle.putInt(EXTRA_TOOL_BAR_COLOR, color);
+        }
+
+        /**
+         * @param color - desired resolved color of the statusbar
+         */
+        public void setStatusBarColor(@ColorInt int color) {
+            mOptionBundle.putInt(EXTRA_STATUS_BAR_COLOR, color);
+        }
+
+        /**
+         * @param color - desired resolved color of the active and selected widget (default is orange) and progress wheel middle line
+         */
+        public void setActiveWidgetColor(@ColorInt int color) {
+            mOptionBundle.putInt(EXTRA_UCROP_COLOR_WIDGET_ACTIVE, color);
+        }
+
+        /**
+         * @param color - desired resolved color of Toolbar text and buttons (default is darker orange)
+         */
+        public void setToolbarWidgetColor(@ColorInt int color) {
+            mOptionBundle.putInt(EXTRA_UCROP_WIDGET_COLOR_TOOLBAR, color);
+        }
+
+        /**
+         * @param text - desired text for Toolbar title
+         */
+        public void setToolbarTitle(@Nullable String text) {
+            mOptionBundle.putString(EXTRA_UCROP_TITLE_TEXT_TOOLBAR, text);
+        }
+
+        /**
+         * @param drawable - desired drawable for the Toolbar left cancel icon
+         */
+        public void setToolbarCancelDrawable(@DrawableRes int drawable) {
+            mOptionBundle.putInt(EXTRA_UCROP_WIDGET_CANCEL_DRAWABLE, drawable);
+        }
+
+        /**
+         * @param drawable - desired drawable for the Toolbar right crop icon
+         */
+        public void setToolbarCropDrawable(@DrawableRes int drawable) {
+            mOptionBundle.putInt(EXTRA_UCROP_WIDGET_CROP_DRAWABLE, drawable);
+        }
+
+        /**
+         * @param color - desired resolved color of logo fill (default is darker grey)
+         */
+        public void setLogoColor(@ColorInt int color) {
+            mOptionBundle.putInt(EXTRA_UCROP_LOGO_COLOR, color);
+        }
+
+        /**
+         * @param hide - set to true to hide the bottom controls (shown by default)
+         */
+        public void setHideBottomControls(boolean hide) {
+            mOptionBundle.putBoolean(EXTRA_HIDE_BOTTOM_CONTROLS, hide);
+        }
+
+        /**
+         * @param enabled - set to true to let user resize crop bounds (disabled by default)
+         */
+        public void setFreeStyleCropEnabled(boolean enabled) {
+            mOptionBundle.putBoolean(EXTRA_FREE_STYLE_CROP, enabled);
+        }
+
+        /**
+         * Pass an ordered list of desired aspect ratios that should be available for a user.
+         *
+         * @param selectedByDefault - index of aspect ratio option that is selected by default (starts with 0).
+         * @param aspectRatio       - list of aspect ratio options that are available to user
+         */
+        public void setAspectRatioOptions(int selectedByDefault, AspectRatio... aspectRatio) {
+            if (selectedByDefault > aspectRatio.length) {
+                throw new IllegalArgumentException(String.format(Locale.US,
+                        "Index [selectedByDefault = %d] cannot be higher than aspect ratio options count [count = %d].",
+                        selectedByDefault, aspectRatio.length));
+            }
+            mOptionBundle.putInt(EXTRA_ASPECT_RATIO_SELECTED_BY_DEFAULT, selectedByDefault);
+            mOptionBundle.putParcelableArrayList(EXTRA_ASPECT_RATIO_OPTIONS, new ArrayList<Parcelable>(Arrays.asList(aspectRatio)));
+        }
+
+        /**
+         * @param color - desired background color that should be applied to the root view
+         */
+        public void setRootViewBackgroundColor(@ColorInt int color) {
+            mOptionBundle.putInt(EXTRA_UCROP_ROOT_VIEW_BACKGROUND_COLOR, color);
+        }
+
+        /**
+         * Set an aspect ratio for crop bounds.
+         * User won't see the menu with other ratios options.
+         *
+         * @param x aspect ratio X
+         * @param y aspect ratio Y
+         */
+        public void withAspectRatio(float x, float y) {
+            mOptionBundle.putFloat(EXTRA_ASPECT_RATIO_X, x);
+            mOptionBundle.putFloat(EXTRA_ASPECT_RATIO_Y, y);
+        }
+
+        /**
+         * Set an aspect ratio for crop bounds that is evaluated from source image width and height.
+         * User won't see the menu with other ratios options.
+         */
+        public void useSourceImageAspectRatio() {
+            mOptionBundle.putFloat(EXTRA_ASPECT_RATIO_X, 0);
+            mOptionBundle.putFloat(EXTRA_ASPECT_RATIO_Y, 0);
+        }
+
+        /**
+         * Set maximum size for result cropped image.
+         *
+         * @param width  max cropped image width
+         * @param height max cropped image height
+         */
+        public void withMaxResultSize(@IntRange(from = 100) int width, @IntRange(from = 100) int height) {
+            mOptionBundle.putInt(EXTRA_MAX_SIZE_X, width);
+            mOptionBundle.putInt(EXTRA_MAX_SIZE_Y, height);
+        }
+
+    }
+
+}

+ 643 - 0
ucrop/src/main/java/com/yalantis/ucrop/UCropActivity.java

@@ -0,0 +1,643 @@
+package com.yalantis.ucrop;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Animatable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.ColorInt;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.IdRes;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.AccelerateInterpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.yalantis.ucrop.callback.BitmapCropCallback;
+import com.yalantis.ucrop.model.AspectRatio;
+import com.yalantis.ucrop.util.SelectedStateListDrawable;
+import com.yalantis.ucrop.view.CropImageView;
+import com.yalantis.ucrop.view.GestureCropImageView;
+import com.yalantis.ucrop.view.OverlayView;
+import com.yalantis.ucrop.view.TransformImageView;
+import com.yalantis.ucrop.view.UCropView;
+import com.yalantis.ucrop.view.widget.AspectRatioTextView;
+import com.yalantis.ucrop.view.widget.HorizontalProgressWheelView;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ */
+
+@SuppressWarnings("ConstantConditions")
+public class UCropActivity extends AppCompatActivity {
+
+    public static final int DEFAULT_COMPRESS_QUALITY = 90;
+    public static final Bitmap.CompressFormat DEFAULT_COMPRESS_FORMAT = Bitmap.CompressFormat.JPEG;
+
+    public static final int NONE = 0;
+    public static final int SCALE = 1;
+    public static final int ROTATE = 2;
+    public static final int ALL = 3;
+
+    @IntDef({NONE, SCALE, ROTATE, ALL})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface GestureTypes {
+
+    }
+
+    private static final String TAG = "UCropActivity";
+
+    private static final int TABS_COUNT = 3;
+    private static final int SCALE_WIDGET_SENSITIVITY_COEFFICIENT = 15000;
+    private static final int ROTATE_WIDGET_SENSITIVITY_COEFFICIENT = 42;
+
+    private String mToolbarTitle;
+
+    // Enables dynamic coloring
+    private int mToolbarColor;
+    private int mStatusBarColor;
+    private int mActiveWidgetColor;
+    private int mToolbarWidgetColor;
+    @ColorInt private int mRootViewBackgroundColor;
+    @DrawableRes private int mToolbarCancelDrawable;
+    @DrawableRes private int mToolbarCropDrawable;
+    private int mLogoColor;
+
+    private boolean mShowBottomControls;
+    private boolean mShowLoader = true;
+
+    private UCropView mUCropView;
+    private GestureCropImageView mGestureCropImageView;
+    private OverlayView mOverlayView;
+    private ViewGroup mWrapperStateAspectRatio, mWrapperStateRotate, mWrapperStateScale;
+    private ViewGroup mLayoutAspectRatio, mLayoutRotate, mLayoutScale;
+    private List<ViewGroup> mCropAspectRatioViews = new ArrayList<>();
+    private TextView mTextViewRotateAngle, mTextViewScalePercent;
+    private View mBlockingView;
+
+    private Bitmap.CompressFormat mCompressFormat = DEFAULT_COMPRESS_FORMAT;
+    private int mCompressQuality = DEFAULT_COMPRESS_QUALITY;
+    private int[] mAllowedGestures = new int[]{SCALE, ROTATE, ALL};
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.ucrop_activity_photobox);
+
+        final Intent intent = getIntent();
+
+        setupViews(intent);
+        setImageData(intent);
+        setInitialState();
+        addBlockingView();
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(final Menu menu) {
+        getMenuInflater().inflate(R.menu.ucrop_menu_activity, menu);
+
+        // Change crop & loader menu icons color to match the rest of the UI colors
+
+        MenuItem menuItemLoader = menu.findItem(R.id.menu_loader);
+        Drawable menuItemLoaderIcon = menuItemLoader.getIcon();
+        if (menuItemLoaderIcon != null) {
+            try {
+                menuItemLoaderIcon.mutate();
+                menuItemLoaderIcon.setColorFilter(mToolbarWidgetColor, PorterDuff.Mode.SRC_ATOP);
+                menuItemLoader.setIcon(menuItemLoaderIcon);
+            } catch (IllegalStateException e) {
+                Log.i(TAG, String.format("%s - %s", e.getMessage(), getString(R.string.ucrop_mutate_exception_hint)));
+            }
+            ((Animatable) menuItemLoader.getIcon()).start();
+        }
+
+        MenuItem menuItemCrop = menu.findItem(R.id.menu_crop);
+        Drawable menuItemCropIcon = ContextCompat.getDrawable(this, mToolbarCropDrawable);
+        if (menuItemCropIcon != null) {
+            menuItemCropIcon.mutate();
+            menuItemCropIcon.setColorFilter(mToolbarWidgetColor, PorterDuff.Mode.SRC_ATOP);
+            menuItemCrop.setIcon(menuItemCropIcon);
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        menu.findItem(R.id.menu_crop).setVisible(!mShowLoader);
+        menu.findItem(R.id.menu_loader).setVisible(mShowLoader);
+        return super.onPrepareOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == R.id.menu_crop) {
+            cropAndSaveImage();
+        } else if (item.getItemId() == android.R.id.home) {
+            onBackPressed();
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        if (mGestureCropImageView != null) {
+            mGestureCropImageView.cancelAllAnimations();
+        }
+    }
+
+    /**
+     * This method extracts all data from the incoming intent and setups views properly.
+     */
+    private void setImageData(@NonNull Intent intent) {
+        Uri inputUri = intent.getParcelableExtra(UCrop.EXTRA_INPUT_URI);
+        Uri outputUri = intent.getParcelableExtra(UCrop.EXTRA_OUTPUT_URI);
+        processOptions(intent);
+
+        if (inputUri != null && outputUri != null) {
+            try {
+                mGestureCropImageView.setImageUri(inputUri, outputUri);
+            } catch (Exception e) {
+                setResultError(e);
+                finish();
+            }
+        } else {
+            setResultError(new NullPointerException(getString(R.string.ucrop_error_input_data_is_absent)));
+            finish();
+        }
+    }
+
+    /**
+     * This method extracts {@link com.yalantis.ucrop.UCrop.Options #optionsBundle} from incoming intent
+     * and setups Activity, {@link OverlayView} and {@link CropImageView} properly.
+     */
+    @SuppressWarnings("deprecation")
+    private void processOptions(@NonNull Intent intent) {
+        // Bitmap compression options
+        String compressionFormatName = intent.getStringExtra(UCrop.Options.EXTRA_COMPRESSION_FORMAT_NAME);
+        Bitmap.CompressFormat compressFormat = null;
+        if (!TextUtils.isEmpty(compressionFormatName)) {
+            compressFormat = Bitmap.CompressFormat.valueOf(compressionFormatName);
+        }
+        mCompressFormat = (compressFormat == null) ? DEFAULT_COMPRESS_FORMAT : compressFormat;
+
+        mCompressQuality = intent.getIntExtra(UCrop.Options.EXTRA_COMPRESSION_QUALITY, UCropActivity.DEFAULT_COMPRESS_QUALITY);
+
+        // Gestures options
+        int[] allowedGestures = intent.getIntArrayExtra(UCrop.Options.EXTRA_ALLOWED_GESTURES);
+        if (allowedGestures != null && allowedGestures.length == TABS_COUNT) {
+            mAllowedGestures = allowedGestures;
+        }
+
+        // Crop image view options
+        mGestureCropImageView.setMaxBitmapSize(intent.getIntExtra(UCrop.Options.EXTRA_MAX_BITMAP_SIZE, CropImageView.DEFAULT_MAX_BITMAP_SIZE));
+        mGestureCropImageView.setMaxScaleMultiplier(intent.getFloatExtra(UCrop.Options.EXTRA_MAX_SCALE_MULTIPLIER, CropImageView.DEFAULT_MAX_SCALE_MULTIPLIER));
+        mGestureCropImageView.setImageToWrapCropBoundsAnimDuration(intent.getIntExtra(UCrop.Options.EXTRA_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION, CropImageView.DEFAULT_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION));
+
+        // Overlay view options
+        mOverlayView.setFreestyleCropEnabled(intent.getBooleanExtra(UCrop.Options.EXTRA_FREE_STYLE_CROP, OverlayView.DEFAULT_FREESTYLE_CROP_MODE != OverlayView.FREESTYLE_CROP_MODE_DISABLE));
+
+        mOverlayView.setDimmedColor(intent.getIntExtra(UCrop.Options.EXTRA_DIMMED_LAYER_COLOR, getResources().getColor(R.color.ucrop_color_default_dimmed)));
+        mOverlayView.setCircleDimmedLayer(intent.getBooleanExtra(UCrop.Options.EXTRA_CIRCLE_DIMMED_LAYER, OverlayView.DEFAULT_CIRCLE_DIMMED_LAYER));
+
+        mOverlayView.setShowCropFrame(intent.getBooleanExtra(UCrop.Options.EXTRA_SHOW_CROP_FRAME, OverlayView.DEFAULT_SHOW_CROP_FRAME));
+        mOverlayView.setCropFrameColor(intent.getIntExtra(UCrop.Options.EXTRA_CROP_FRAME_COLOR, getResources().getColor(R.color.ucrop_color_default_crop_frame)));
+        mOverlayView.setCropFrameStrokeWidth(intent.getIntExtra(UCrop.Options.EXTRA_CROP_FRAME_STROKE_WIDTH, getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_frame_stoke_width)));
+
+        mOverlayView.setShowCropGrid(intent.getBooleanExtra(UCrop.Options.EXTRA_SHOW_CROP_GRID, OverlayView.DEFAULT_SHOW_CROP_GRID));
+        mOverlayView.setCropGridRowCount(intent.getIntExtra(UCrop.Options.EXTRA_CROP_GRID_ROW_COUNT, OverlayView.DEFAULT_CROP_GRID_ROW_COUNT));
+        mOverlayView.setCropGridColumnCount(intent.getIntExtra(UCrop.Options.EXTRA_CROP_GRID_COLUMN_COUNT, OverlayView.DEFAULT_CROP_GRID_COLUMN_COUNT));
+        mOverlayView.setCropGridColor(intent.getIntExtra(UCrop.Options.EXTRA_CROP_GRID_COLOR, getResources().getColor(R.color.ucrop_color_default_crop_grid)));
+        mOverlayView.setCropGridStrokeWidth(intent.getIntExtra(UCrop.Options.EXTRA_CROP_GRID_STROKE_WIDTH, getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_grid_stoke_width)));
+
+        // Aspect ratio options
+        float aspectRatioX = intent.getFloatExtra(UCrop.EXTRA_ASPECT_RATIO_X, 0);
+        float aspectRatioY = intent.getFloatExtra(UCrop.EXTRA_ASPECT_RATIO_Y, 0);
+
+        int aspectRationSelectedByDefault = intent.getIntExtra(UCrop.Options.EXTRA_ASPECT_RATIO_SELECTED_BY_DEFAULT, 0);
+        ArrayList<AspectRatio> aspectRatioList = intent.getParcelableArrayListExtra(UCrop.Options.EXTRA_ASPECT_RATIO_OPTIONS);
+
+        if (aspectRatioX > 0 && aspectRatioY > 0) {
+            if (mWrapperStateAspectRatio != null) {
+                mWrapperStateAspectRatio.setVisibility(View.GONE);
+            }
+            mGestureCropImageView.setTargetAspectRatio(aspectRatioX / aspectRatioY);
+        } else if (aspectRatioList != null && aspectRationSelectedByDefault < aspectRatioList.size()) {
+            mGestureCropImageView.setTargetAspectRatio(aspectRatioList.get(aspectRationSelectedByDefault).getAspectRatioX() /
+                    aspectRatioList.get(aspectRationSelectedByDefault).getAspectRatioY());
+        } else {
+            mGestureCropImageView.setTargetAspectRatio(CropImageView.SOURCE_IMAGE_ASPECT_RATIO);
+        }
+
+        // Result bitmap max size options
+        int maxSizeX = intent.getIntExtra(UCrop.EXTRA_MAX_SIZE_X, 0);
+        int maxSizeY = intent.getIntExtra(UCrop.EXTRA_MAX_SIZE_Y, 0);
+
+        if (maxSizeX > 0 && maxSizeY > 0) {
+            mGestureCropImageView.setMaxResultImageSizeX(maxSizeX);
+            mGestureCropImageView.setMaxResultImageSizeY(maxSizeY);
+        }
+    }
+
+    private void setupViews(@NonNull Intent intent) {
+        mStatusBarColor = intent.getIntExtra(UCrop.Options.EXTRA_STATUS_BAR_COLOR, ContextCompat.getColor(this, R.color.ucrop_color_statusbar));
+        mToolbarColor = intent.getIntExtra(UCrop.Options.EXTRA_TOOL_BAR_COLOR, ContextCompat.getColor(this, R.color.ucrop_color_toolbar));
+        mActiveWidgetColor = intent.getIntExtra(UCrop.Options.EXTRA_UCROP_COLOR_WIDGET_ACTIVE, ContextCompat.getColor(this, R.color.ucrop_color_widget_active));
+        mToolbarWidgetColor = intent.getIntExtra(UCrop.Options.EXTRA_UCROP_WIDGET_COLOR_TOOLBAR, ContextCompat.getColor(this, R.color.ucrop_color_toolbar_widget));
+        mToolbarCancelDrawable = intent.getIntExtra(UCrop.Options.EXTRA_UCROP_WIDGET_CANCEL_DRAWABLE, R.drawable.ucrop_ic_cross);
+        mToolbarCropDrawable = intent.getIntExtra(UCrop.Options.EXTRA_UCROP_WIDGET_CROP_DRAWABLE, R.drawable.ucrop_ic_done);
+        mToolbarTitle = intent.getStringExtra(UCrop.Options.EXTRA_UCROP_TITLE_TEXT_TOOLBAR);
+        mToolbarTitle = mToolbarTitle != null ? mToolbarTitle : getResources().getString(R.string.ucrop_label_edit_photo);
+        mLogoColor = intent.getIntExtra(UCrop.Options.EXTRA_UCROP_LOGO_COLOR, ContextCompat.getColor(this, R.color.ucrop_color_default_logo));
+        mShowBottomControls = !intent.getBooleanExtra(UCrop.Options.EXTRA_HIDE_BOTTOM_CONTROLS, false);
+        mRootViewBackgroundColor = intent.getIntExtra(UCrop.Options.EXTRA_UCROP_ROOT_VIEW_BACKGROUND_COLOR, ContextCompat.getColor(this, R.color.ucrop_color_crop_background));
+
+        setupAppBar();
+        initiateRootViews();
+
+        if (mShowBottomControls) {
+            ViewGroup photoBox = (ViewGroup) findViewById(R.id.ucrop_photobox);
+            View.inflate(this, R.layout.ucrop_controls, photoBox);
+
+            mWrapperStateAspectRatio = (ViewGroup) findViewById(R.id.state_aspect_ratio);
+            mWrapperStateAspectRatio.setOnClickListener(mStateClickListener);
+            mWrapperStateRotate = (ViewGroup) findViewById(R.id.state_rotate);
+            mWrapperStateRotate.setOnClickListener(mStateClickListener);
+            mWrapperStateScale = (ViewGroup) findViewById(R.id.state_scale);
+            mWrapperStateScale.setOnClickListener(mStateClickListener);
+
+            mLayoutAspectRatio = (ViewGroup) findViewById(R.id.layout_aspect_ratio);
+            mLayoutRotate = (ViewGroup) findViewById(R.id.layout_rotate_wheel);
+            mLayoutScale = (ViewGroup) findViewById(R.id.layout_scale_wheel);
+
+            setupAspectRatioWidget(intent);
+            setupRotateWidget();
+            setupScaleWidget();
+            setupStatesWrapper();
+        }
+    }
+
+    /**
+     * Configures and styles both status bar and toolbar.
+     */
+    private void setupAppBar() {
+        setStatusBarColor(mStatusBarColor);
+
+        final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+
+        // Set all of the Toolbar coloring
+        toolbar.setBackgroundColor(mToolbarColor);
+        toolbar.setTitleTextColor(mToolbarWidgetColor);
+
+        final TextView toolbarTitle = (TextView) toolbar.findViewById(R.id.toolbar_title);
+        toolbarTitle.setTextColor(mToolbarWidgetColor);
+        toolbarTitle.setText(mToolbarTitle);
+
+        // Color buttons inside the Toolbar
+        Drawable stateButtonDrawable = ContextCompat.getDrawable(this, mToolbarCancelDrawable).mutate();
+        stateButtonDrawable.setColorFilter(mToolbarWidgetColor, PorterDuff.Mode.SRC_ATOP);
+        toolbar.setNavigationIcon(stateButtonDrawable);
+
+        setSupportActionBar(toolbar);
+        final ActionBar actionBar = getSupportActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayShowTitleEnabled(false);
+        }
+    }
+
+    private void initiateRootViews() {
+        mUCropView = (UCropView) findViewById(R.id.ucrop);
+        mGestureCropImageView = mUCropView.getCropImageView();
+        mOverlayView = mUCropView.getOverlayView();
+
+        mGestureCropImageView.setTransformImageListener(mImageListener);
+
+        ((ImageView) findViewById(R.id.image_view_logo)).setColorFilter(mLogoColor, PorterDuff.Mode.SRC_ATOP);
+
+        findViewById(R.id.ucrop_frame).setBackgroundColor(mRootViewBackgroundColor);
+    }
+
+    private TransformImageView.TransformImageListener mImageListener = new TransformImageView.TransformImageListener() {
+        @Override
+        public void onRotate(float currentAngle) {
+            setAngleText(currentAngle);
+        }
+
+        @Override
+        public void onScale(float currentScale) {
+            setScaleText(currentScale);
+        }
+
+        @Override
+        public void onLoadComplete() {
+            mUCropView.animate().alpha(1).setDuration(300).setInterpolator(new AccelerateInterpolator());
+            mBlockingView.setClickable(false);
+            mShowLoader = false;
+            supportInvalidateOptionsMenu();
+        }
+
+        @Override
+        public void onLoadFailure(@NonNull Exception e) {
+            setResultError(e);
+            finish();
+        }
+
+    };
+
+    /**
+     * Use {@link #mActiveWidgetColor} for color filter
+     */
+    private void setupStatesWrapper() {
+        ImageView stateScaleImageView = (ImageView) findViewById(R.id.image_view_state_scale);
+        ImageView stateRotateImageView = (ImageView) findViewById(R.id.image_view_state_rotate);
+        ImageView stateAspectRatioImageView = (ImageView) findViewById(R.id.image_view_state_aspect_ratio);
+
+        stateScaleImageView.setImageDrawable(new SelectedStateListDrawable(stateScaleImageView.getDrawable(), mActiveWidgetColor));
+        stateRotateImageView.setImageDrawable(new SelectedStateListDrawable(stateRotateImageView.getDrawable(), mActiveWidgetColor));
+        stateAspectRatioImageView.setImageDrawable(new SelectedStateListDrawable(stateAspectRatioImageView.getDrawable(), mActiveWidgetColor));
+    }
+
+
+    /**
+     * Sets status-bar color for L devices.
+     *
+     * @param color - status-bar color
+     */
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    private void setStatusBarColor(@ColorInt int color) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            final Window window = getWindow();
+            if (window != null) {
+                window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+                window.setStatusBarColor(color);
+            }
+        }
+    }
+
+    private void setupAspectRatioWidget(@NonNull Intent intent) {
+
+        int aspectRationSelectedByDefault = intent.getIntExtra(UCrop.Options.EXTRA_ASPECT_RATIO_SELECTED_BY_DEFAULT, 0);
+        ArrayList<AspectRatio> aspectRatioList = intent.getParcelableArrayListExtra(UCrop.Options.EXTRA_ASPECT_RATIO_OPTIONS);
+
+        if (aspectRatioList == null || aspectRatioList.isEmpty()) {
+            aspectRationSelectedByDefault = 2;
+
+            aspectRatioList = new ArrayList<>();
+            aspectRatioList.add(new AspectRatio(null, 1, 1));
+            aspectRatioList.add(new AspectRatio(null, 3, 4));
+            aspectRatioList.add(new AspectRatio(getString(R.string.ucrop_label_original).toUpperCase(),
+                    CropImageView.SOURCE_IMAGE_ASPECT_RATIO, CropImageView.SOURCE_IMAGE_ASPECT_RATIO));
+            aspectRatioList.add(new AspectRatio(null, 3, 2));
+            aspectRatioList.add(new AspectRatio(null, 16, 9));
+        }
+
+        LinearLayout wrapperAspectRatioList = (LinearLayout) findViewById(R.id.layout_aspect_ratio);
+
+        FrameLayout wrapperAspectRatio;
+        AspectRatioTextView aspectRatioTextView;
+        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT);
+        lp.weight = 1;
+        for (AspectRatio aspectRatio : aspectRatioList) {
+            wrapperAspectRatio = (FrameLayout) getLayoutInflater().inflate(R.layout.ucrop_aspect_ratio, null);
+            wrapperAspectRatio.setLayoutParams(lp);
+            aspectRatioTextView = ((AspectRatioTextView) wrapperAspectRatio.getChildAt(0));
+            aspectRatioTextView.setActiveColor(mActiveWidgetColor);
+            aspectRatioTextView.setAspectRatio(aspectRatio);
+
+            wrapperAspectRatioList.addView(wrapperAspectRatio);
+            mCropAspectRatioViews.add(wrapperAspectRatio);
+        }
+
+        mCropAspectRatioViews.get(aspectRationSelectedByDefault).setSelected(true);
+
+        for (ViewGroup cropAspectRatioView : mCropAspectRatioViews) {
+            cropAspectRatioView.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mGestureCropImageView.setTargetAspectRatio(
+                            ((AspectRatioTextView) ((ViewGroup) v).getChildAt(0)).getAspectRatio(v.isSelected()));
+                    mGestureCropImageView.setImageToWrapCropBounds();
+                    if (!v.isSelected()) {
+                        for (ViewGroup cropAspectRatioView : mCropAspectRatioViews) {
+                            cropAspectRatioView.setSelected(cropAspectRatioView == v);
+                        }
+                    }
+                }
+            });
+        }
+    }
+
+    private void setupRotateWidget() {
+        mTextViewRotateAngle = ((TextView) findViewById(R.id.text_view_rotate));
+        ((HorizontalProgressWheelView) findViewById(R.id.rotate_scroll_wheel))
+                .setScrollingListener(new HorizontalProgressWheelView.ScrollingListener() {
+                    @Override
+                    public void onScroll(float delta, float totalDistance) {
+                        mGestureCropImageView.postRotate(delta / ROTATE_WIDGET_SENSITIVITY_COEFFICIENT);
+                    }
+
+                    @Override
+                    public void onScrollEnd() {
+                        mGestureCropImageView.setImageToWrapCropBounds();
+                    }
+
+                    @Override
+                    public void onScrollStart() {
+                        mGestureCropImageView.cancelAllAnimations();
+                    }
+                });
+
+        ((HorizontalProgressWheelView) findViewById(R.id.rotate_scroll_wheel)).setMiddleLineColor(mActiveWidgetColor);
+
+
+        findViewById(R.id.wrapper_reset_rotate).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                resetRotation();
+            }
+        });
+        findViewById(R.id.wrapper_rotate_by_angle).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                rotateByAngle(90);
+            }
+        });
+    }
+
+    private void setupScaleWidget() {
+        mTextViewScalePercent = ((TextView) findViewById(R.id.text_view_scale));
+        ((HorizontalProgressWheelView) findViewById(R.id.scale_scroll_wheel))
+                .setScrollingListener(new HorizontalProgressWheelView.ScrollingListener() {
+                    @Override
+                    public void onScroll(float delta, float totalDistance) {
+                        if (delta > 0) {
+                            mGestureCropImageView.zoomInImage(mGestureCropImageView.getCurrentScale()
+                                    + delta * ((mGestureCropImageView.getMaxScale() - mGestureCropImageView.getMinScale()) / SCALE_WIDGET_SENSITIVITY_COEFFICIENT));
+                        } else {
+                            mGestureCropImageView.zoomOutImage(mGestureCropImageView.getCurrentScale()
+                                    + delta * ((mGestureCropImageView.getMaxScale() - mGestureCropImageView.getMinScale()) / SCALE_WIDGET_SENSITIVITY_COEFFICIENT));
+                        }
+                    }
+
+                    @Override
+                    public void onScrollEnd() {
+                        mGestureCropImageView.setImageToWrapCropBounds();
+                    }
+
+                    @Override
+                    public void onScrollStart() {
+                        mGestureCropImageView.cancelAllAnimations();
+                    }
+                });
+        ((HorizontalProgressWheelView) findViewById(R.id.scale_scroll_wheel)).setMiddleLineColor(mActiveWidgetColor);
+    }
+
+    private void setAngleText(float angle) {
+        if (mTextViewRotateAngle != null) {
+            mTextViewRotateAngle.setText(String.format(Locale.getDefault(), "%.1f°", angle));
+        }
+    }
+
+    private void setScaleText(float scale) {
+        if (mTextViewScalePercent != null) {
+            mTextViewScalePercent.setText(String.format(Locale.getDefault(), "%d%%", (int) (scale * 100)));
+        }
+    }
+
+    private void resetRotation() {
+        mGestureCropImageView.postRotate(-mGestureCropImageView.getCurrentAngle());
+        mGestureCropImageView.setImageToWrapCropBounds();
+    }
+
+    private void rotateByAngle(int angle) {
+        mGestureCropImageView.postRotate(angle);
+        mGestureCropImageView.setImageToWrapCropBounds();
+    }
+
+    private final View.OnClickListener mStateClickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            if (!v.isSelected()) {
+                setWidgetState(v.getId());
+            }
+        }
+    };
+
+    private void setInitialState() {
+        if (mShowBottomControls) {
+            if (mWrapperStateAspectRatio.getVisibility() == View.VISIBLE) {
+                setWidgetState(R.id.state_aspect_ratio);
+            } else {
+                setWidgetState(R.id.state_scale);
+            }
+        } else {
+            setAllowedGestures(0);
+        }
+    }
+
+    private void setWidgetState(@IdRes int stateViewId) {
+        if (!mShowBottomControls) return;
+
+        mWrapperStateAspectRatio.setSelected(stateViewId == R.id.state_aspect_ratio);
+        mWrapperStateRotate.setSelected(stateViewId == R.id.state_rotate);
+        mWrapperStateScale.setSelected(stateViewId == R.id.state_scale);
+
+        mLayoutAspectRatio.setVisibility(stateViewId == R.id.state_aspect_ratio ? View.VISIBLE : View.GONE);
+        mLayoutRotate.setVisibility(stateViewId == R.id.state_rotate ? View.VISIBLE : View.GONE);
+        mLayoutScale.setVisibility(stateViewId == R.id.state_scale ? View.VISIBLE : View.GONE);
+
+        if (stateViewId == R.id.state_scale) {
+            setAllowedGestures(0);
+        } else if (stateViewId == R.id.state_rotate) {
+            setAllowedGestures(1);
+        } else {
+            setAllowedGestures(2);
+        }
+    }
+
+    private void setAllowedGestures(int tab) {
+        mGestureCropImageView.setScaleEnabled(mAllowedGestures[tab] == ALL || mAllowedGestures[tab] == SCALE);
+        mGestureCropImageView.setRotateEnabled(mAllowedGestures[tab] == ALL || mAllowedGestures[tab] == ROTATE);
+    }
+
+    /**
+     * Adds view that covers everything below the Toolbar.
+     * When it's clickable - user won't be able to click/touch anything below the Toolbar.
+     * Need to block user input while loading and cropping an image.
+     */
+    private void addBlockingView() {
+        if (mBlockingView == null) {
+            mBlockingView = new View(this);
+            RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+            lp.addRule(RelativeLayout.BELOW, R.id.toolbar);
+            mBlockingView.setLayoutParams(lp);
+            mBlockingView.setClickable(true);
+        }
+
+        ((RelativeLayout) findViewById(R.id.ucrop_photobox)).addView(mBlockingView);
+    }
+
+    protected void cropAndSaveImage() {
+        mBlockingView.setClickable(true);
+        mShowLoader = true;
+        supportInvalidateOptionsMenu();
+
+        mGestureCropImageView.cropAndSaveImage(mCompressFormat, mCompressQuality, new BitmapCropCallback() {
+
+            @Override
+            public void onBitmapCropped(@NonNull Uri resultUri, int offsetX, int offsetY, int imageWidth, int imageHeight) {
+                setResultUri(resultUri, mGestureCropImageView.getTargetAspectRatio(), offsetX, offsetY, imageWidth, imageHeight);
+                finish();
+            }
+
+            @Override
+            public void onCropFailure(@NonNull Throwable t) {
+                setResultError(t);
+                finish();
+            }
+        });
+    }
+
+    protected void setResultUri(Uri uri, float resultAspectRatio, int offsetX, int offsetY, int imageWidth, int imageHeight) {
+        setResult(RESULT_OK, new Intent()
+                .putExtra(UCrop.EXTRA_OUTPUT_URI, uri)
+                .putExtra(UCrop.EXTRA_OUTPUT_CROP_ASPECT_RATIO, resultAspectRatio)
+                .putExtra(UCrop.EXTRA_OUTPUT_IMAGE_WIDTH, imageWidth)
+                .putExtra(UCrop.EXTRA_OUTPUT_IMAGE_HEIGHT, imageHeight)
+                .putExtra(UCrop.EXTRA_OUTPUT_OFFSET_X, offsetX)
+                .putExtra(UCrop.EXTRA_OUTPUT_OFFSET_Y, offsetY)
+        );
+    }
+
+    protected void setResultError(Throwable throwable) {
+        setResult(UCrop.RESULT_ERROR, new Intent().putExtra(UCrop.EXTRA_ERROR, throwable));
+    }
+
+}

+ 12 - 0
ucrop/src/main/java/com/yalantis/ucrop/callback/BitmapCropCallback.java

@@ -0,0 +1,12 @@
+package com.yalantis.ucrop.callback;
+
+import android.net.Uri;
+import android.support.annotation.NonNull;
+
+public interface BitmapCropCallback {
+
+    void onBitmapCropped(@NonNull Uri resultUri, int offsetX, int offsetY, int imageWidth, int imageHeight);
+
+    void onCropFailure(@NonNull Throwable t);
+
+}

+ 15 - 0
ucrop/src/main/java/com/yalantis/ucrop/callback/BitmapLoadCallback.java

@@ -0,0 +1,15 @@
+package com.yalantis.ucrop.callback;
+
+import android.graphics.Bitmap;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.yalantis.ucrop.model.ExifInfo;
+
+public interface BitmapLoadCallback {
+
+    void onBitmapLoaded(@NonNull Bitmap bitmap, @NonNull ExifInfo exifInfo, @NonNull String imageInputPath, @Nullable String imageOutputPath);
+
+    void onFailure(@NonNull Exception bitmapWorkerException);
+
+}

+ 10 - 0
ucrop/src/main/java/com/yalantis/ucrop/callback/CropBoundsChangeListener.java

@@ -0,0 +1,10 @@
+package com.yalantis.ucrop.callback;
+
+/**
+ * Interface for crop bound change notifying.
+ */
+public interface CropBoundsChangeListener {
+
+    void onCropAspectRatioChanged(float cropRatio);
+
+}

+ 12 - 0
ucrop/src/main/java/com/yalantis/ucrop/callback/OverlayViewChangeListener.java

@@ -0,0 +1,12 @@
+package com.yalantis.ucrop.callback;
+
+import android.graphics.RectF;
+
+/**
+ * Created by Oleksii Shliama.
+ */
+public interface OverlayViewChangeListener {
+
+    void onCropRectUpdated(RectF cropRect);
+
+}

+ 66 - 0
ucrop/src/main/java/com/yalantis/ucrop/model/AspectRatio.java

@@ -0,0 +1,66 @@
+package com.yalantis.ucrop.model;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.Nullable;
+
+/**
+ * Created by Oleksii Shliama [https://github.com/shliama] on 6/24/16.
+ */
+public class AspectRatio implements Parcelable {
+
+    @Nullable
+    private final String mAspectRatioTitle;
+    private final float mAspectRatioX;
+    private final float mAspectRatioY;
+
+    public AspectRatio(@Nullable String aspectRatioTitle, float aspectRatioX, float aspectRatioY) {
+        mAspectRatioTitle = aspectRatioTitle;
+        mAspectRatioX = aspectRatioX;
+        mAspectRatioY = aspectRatioY;
+    }
+
+    protected AspectRatio(Parcel in) {
+        mAspectRatioTitle = in.readString();
+        mAspectRatioX = in.readFloat();
+        mAspectRatioY = in.readFloat();
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mAspectRatioTitle);
+        dest.writeFloat(mAspectRatioX);
+        dest.writeFloat(mAspectRatioY);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Creator<AspectRatio> CREATOR = new Creator<AspectRatio>() {
+        @Override
+        public AspectRatio createFromParcel(Parcel in) {
+            return new AspectRatio(in);
+        }
+
+        @Override
+        public AspectRatio[] newArray(int size) {
+            return new AspectRatio[size];
+        }
+    };
+
+    @Nullable
+    public String getAspectRatioTitle() {
+        return mAspectRatioTitle;
+    }
+
+    public float getAspectRatioX() {
+        return mAspectRatioX;
+    }
+
+    public float getAspectRatioY() {
+        return mAspectRatioY;
+    }
+
+}

+ 58 - 0
ucrop/src/main/java/com/yalantis/ucrop/model/CropParameters.java

@@ -0,0 +1,58 @@
+package com.yalantis.ucrop.model;
+
+import android.graphics.Bitmap;
+
+/**
+ * Created by Oleksii Shliama [https://github.com/shliama] on 6/21/16.
+ */
+public class CropParameters {
+
+    private int mMaxResultImageSizeX, mMaxResultImageSizeY;
+
+    private Bitmap.CompressFormat mCompressFormat;
+    private int mCompressQuality;
+    private String mImageInputPath, mImageOutputPath;
+    private ExifInfo mExifInfo;
+
+
+    public CropParameters(int maxResultImageSizeX, int maxResultImageSizeY,
+                          Bitmap.CompressFormat compressFormat, int compressQuality,
+                          String imageInputPath, String imageOutputPath, ExifInfo exifInfo) {
+        mMaxResultImageSizeX = maxResultImageSizeX;
+        mMaxResultImageSizeY = maxResultImageSizeY;
+        mCompressFormat = compressFormat;
+        mCompressQuality = compressQuality;
+        mImageInputPath = imageInputPath;
+        mImageOutputPath = imageOutputPath;
+        mExifInfo = exifInfo;
+    }
+
+    public int getMaxResultImageSizeX() {
+        return mMaxResultImageSizeX;
+    }
+
+    public int getMaxResultImageSizeY() {
+        return mMaxResultImageSizeY;
+    }
+
+    public Bitmap.CompressFormat getCompressFormat() {
+        return mCompressFormat;
+    }
+
+    public int getCompressQuality() {
+        return mCompressQuality;
+    }
+
+    public String getImageInputPath() {
+        return mImageInputPath;
+    }
+
+    public String getImageOutputPath() {
+        return mImageOutputPath;
+    }
+
+    public ExifInfo getExifInfo() {
+        return mExifInfo;
+    }
+
+}

+ 63 - 0
ucrop/src/main/java/com/yalantis/ucrop/model/ExifInfo.java

@@ -0,0 +1,63 @@
+package com.yalantis.ucrop.model;
+
+/**
+ * Created by Oleksii Shliama [https://github.com/shliama] on 6/21/16.
+ */
+public class ExifInfo {
+
+    private int mExifOrientation;
+    private int mExifDegrees;
+    private int mExifTranslation;
+
+    public ExifInfo(int exifOrientation, int exifDegrees, int exifTranslation) {
+        mExifOrientation = exifOrientation;
+        mExifDegrees = exifDegrees;
+        mExifTranslation = exifTranslation;
+    }
+
+    public int getExifOrientation() {
+        return mExifOrientation;
+    }
+
+    public int getExifDegrees() {
+        return mExifDegrees;
+    }
+
+    public int getExifTranslation() {
+        return mExifTranslation;
+    }
+
+    public void setExifOrientation(int exifOrientation) {
+        mExifOrientation = exifOrientation;
+    }
+
+    public void setExifDegrees(int exifDegrees) {
+        mExifDegrees = exifDegrees;
+    }
+
+    public void setExifTranslation(int exifTranslation) {
+        mExifTranslation = exifTranslation;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        ExifInfo exifInfo = (ExifInfo) o;
+
+        if (mExifOrientation != exifInfo.mExifOrientation) return false;
+        if (mExifDegrees != exifInfo.mExifDegrees) return false;
+        return mExifTranslation == exifInfo.mExifTranslation;
+
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mExifOrientation;
+        result = 31 * result + mExifDegrees;
+        result = 31 * result + mExifTranslation;
+        return result;
+    }
+
+}

+ 37 - 0
ucrop/src/main/java/com/yalantis/ucrop/model/ImageState.java

@@ -0,0 +1,37 @@
+package com.yalantis.ucrop.model;
+
+import android.graphics.RectF;
+
+/**
+ * Created by Oleksii Shliama [https://github.com/shliama] on 6/21/16.
+ */
+public class ImageState {
+
+    private RectF mCropRect;
+    private RectF mCurrentImageRect;
+
+    private float mCurrentScale, mCurrentAngle;
+
+    public ImageState(RectF cropRect, RectF currentImageRect, float currentScale, float currentAngle) {
+        mCropRect = cropRect;
+        mCurrentImageRect = currentImageRect;
+        mCurrentScale = currentScale;
+        mCurrentAngle = currentAngle;
+    }
+
+    public RectF getCropRect() {
+        return mCropRect;
+    }
+
+    public RectF getCurrentImageRect() {
+        return mCurrentImageRect;
+    }
+
+    public float getCurrentScale() {
+        return mCurrentScale;
+    }
+
+    public float getCurrentAngle() {
+        return mCurrentAngle;
+    }
+}

+ 195 - 0
ucrop/src/main/java/com/yalantis/ucrop/task/BitmapCropTask.java

@@ -0,0 +1,195 @@
+package com.yalantis.ucrop.task;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.RectF;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import com.yalantis.ucrop.callback.BitmapCropCallback;
+import com.yalantis.ucrop.model.CropParameters;
+import com.yalantis.ucrop.model.ExifInfo;
+import com.yalantis.ucrop.model.ImageState;
+import com.yalantis.ucrop.util.FileUtils;
+import com.yalantis.ucrop.util.ImageHeaderParser;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Crops part of image that fills the crop bounds.
+ * <p/>
+ * First image is downscaled if max size was set and if resulting image is larger that max size.
+ * Then image is rotated accordingly.
+ * Finally new Bitmap object is created and saved to file.
+ */
+public class BitmapCropTask extends AsyncTask<Void, Void, Throwable> {
+
+    private static final String TAG = "BitmapCropTask";
+
+    static {
+        System.loadLibrary("ucrop");
+    }
+
+    private Bitmap mViewBitmap;
+
+    private final RectF mCropRect;
+    private final RectF mCurrentImageRect;
+
+    private float mCurrentScale, mCurrentAngle;
+    private final int mMaxResultImageSizeX, mMaxResultImageSizeY;
+
+    private final Bitmap.CompressFormat mCompressFormat;
+    private final int mCompressQuality;
+    private final String mImageInputPath, mImageOutputPath;
+    private final ExifInfo mExifInfo;
+    private final BitmapCropCallback mCropCallback;
+
+    private int mCroppedImageWidth, mCroppedImageHeight;
+    private int cropOffsetX, cropOffsetY;
+
+    public BitmapCropTask(@Nullable Bitmap viewBitmap, @NonNull ImageState imageState, @NonNull CropParameters cropParameters,
+                          @Nullable BitmapCropCallback cropCallback) {
+
+        mViewBitmap = viewBitmap;
+        mCropRect = imageState.getCropRect();
+        mCurrentImageRect = imageState.getCurrentImageRect();
+
+        mCurrentScale = imageState.getCurrentScale();
+        mCurrentAngle = imageState.getCurrentAngle();
+        mMaxResultImageSizeX = cropParameters.getMaxResultImageSizeX();
+        mMaxResultImageSizeY = cropParameters.getMaxResultImageSizeY();
+
+        mCompressFormat = cropParameters.getCompressFormat();
+        mCompressQuality = cropParameters.getCompressQuality();
+
+        mImageInputPath = cropParameters.getImageInputPath();
+        mImageOutputPath = cropParameters.getImageOutputPath();
+        mExifInfo = cropParameters.getExifInfo();
+
+        mCropCallback = cropCallback;
+    }
+
+    @Override
+    @Nullable
+    protected Throwable doInBackground(Void... params) {
+        if (mViewBitmap == null) {
+            return new NullPointerException("ViewBitmap is null");
+        } else if (mViewBitmap.isRecycled()) {
+            return new NullPointerException("ViewBitmap is recycled");
+        } else if (mCurrentImageRect.isEmpty()) {
+            return new NullPointerException("CurrentImageRect is empty");
+        }
+
+        float resizeScale = resize();
+
+        try {
+            crop(resizeScale);
+            mViewBitmap = null;
+        } catch (Throwable throwable) {
+            return throwable;
+        }
+
+        return null;
+    }
+
+    private float resize() {
+        final BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inJustDecodeBounds = true;
+        BitmapFactory.decodeFile(mImageInputPath, options);
+
+        boolean swapSides = mExifInfo.getExifDegrees() == 90 || mExifInfo.getExifDegrees() == 270;
+        float scaleX = (swapSides ? options.outHeight : options.outWidth) / (float) mViewBitmap.getWidth();
+        float scaleY = (swapSides ? options.outWidth : options.outHeight) / (float) mViewBitmap.getHeight();
+
+        float resizeScale = Math.min(scaleX, scaleY);
+
+        mCurrentScale /= resizeScale;
+
+        resizeScale = 1;
+        if (mMaxResultImageSizeX > 0 && mMaxResultImageSizeY > 0) {
+            float cropWidth = mCropRect.width() / mCurrentScale;
+            float cropHeight = mCropRect.height() / mCurrentScale;
+
+            if (cropWidth > mMaxResultImageSizeX || cropHeight > mMaxResultImageSizeY) {
+
+                scaleX = mMaxResultImageSizeX / cropWidth;
+                scaleY = mMaxResultImageSizeY / cropHeight;
+                resizeScale = Math.min(scaleX, scaleY);
+
+                mCurrentScale /= resizeScale;
+            }
+        }
+        return resizeScale;
+    }
+
+    private boolean crop(float resizeScale) throws IOException {
+        ExifInterface originalExif = new ExifInterface(mImageInputPath);
+
+        cropOffsetX = Math.round((mCropRect.left - mCurrentImageRect.left) / mCurrentScale);
+        cropOffsetY = Math.round((mCropRect.top - mCurrentImageRect.top) / mCurrentScale);
+        mCroppedImageWidth = Math.round(mCropRect.width() / mCurrentScale);
+        mCroppedImageHeight = Math.round(mCropRect.height() / mCurrentScale);
+
+        boolean shouldCrop = shouldCrop(mCroppedImageWidth, mCroppedImageHeight);
+        Log.i(TAG, "Should crop: " + shouldCrop);
+
+        if (shouldCrop) {
+            boolean cropped = cropCImg(mImageInputPath, mImageOutputPath,
+                    cropOffsetX, cropOffsetY, mCroppedImageWidth, mCroppedImageHeight,
+                    mCurrentAngle, resizeScale, mCompressFormat.ordinal(), mCompressQuality,
+                    mExifInfo.getExifDegrees(), mExifInfo.getExifTranslation());
+            if (cropped && mCompressFormat.equals(Bitmap.CompressFormat.JPEG)) {
+                ImageHeaderParser.copyExif(originalExif, mCroppedImageWidth, mCroppedImageHeight, mImageOutputPath);
+            }
+            return cropped;
+        } else {
+            FileUtils.copyFile(mImageInputPath, mImageOutputPath);
+            return false;
+        }
+    }
+
+    /**
+     * Check whether an image should be cropped at all or just file can be copied to the destination path.
+     * For each 1000 pixels there is one pixel of error due to matrix calculations etc.
+     *
+     * @param width  - crop area width
+     * @param height - crop area height
+     * @return - true if image must be cropped, false - if original image fits requirements
+     */
+    private boolean shouldCrop(int width, int height) {
+        int pixelError = 1;
+        pixelError += Math.round(Math.max(width, height) / 1000f);
+        return (mMaxResultImageSizeX > 0 && mMaxResultImageSizeY > 0)
+                || Math.abs(mCropRect.left - mCurrentImageRect.left) > pixelError
+                || Math.abs(mCropRect.top - mCurrentImageRect.top) > pixelError
+                || Math.abs(mCropRect.bottom - mCurrentImageRect.bottom) > pixelError
+                || Math.abs(mCropRect.right - mCurrentImageRect.right) > pixelError
+                || mCurrentAngle != 0;
+    }
+
+    @SuppressWarnings("JniMissingFunction")
+    native public static boolean
+    cropCImg(String inputPath, String outputPath,
+             int left, int top, int width, int height,
+             float angle, float resizeScale,
+             int format, int quality,
+             int exifDegrees, int exifTranslation) throws IOException, OutOfMemoryError;
+
+    @Override
+    protected void onPostExecute(@Nullable Throwable t) {
+        if (mCropCallback != null) {
+            if (t == null) {
+                Uri uri = Uri.fromFile(new File(mImageOutputPath));
+                mCropCallback.onBitmapCropped(uri, cropOffsetX, cropOffsetY, mCroppedImageWidth, mCroppedImageHeight);
+            } else {
+                mCropCallback.onCropFailure(t);
+            }
+        }
+    }
+
+}

+ 280 - 0
ucrop/src/main/java/com/yalantis/ucrop/task/BitmapLoadTask.java

@@ -0,0 +1,280 @@
+package com.yalantis.ucrop.task;
+
+import android.Manifest.permission;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.yalantis.ucrop.callback.BitmapLoadCallback;
+import com.yalantis.ucrop.model.ExifInfo;
+import com.yalantis.ucrop.util.BitmapLoadUtils;
+import com.yalantis.ucrop.util.FileUtils;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Sink;
+
+/**
+ * Creates and returns a Bitmap for a given Uri(String url).
+ * inSampleSize is calculated based on requiredWidth property. However can be adjusted if OOM occurs.
+ * If any EXIF config is found - bitmap is transformed properly.
+ */
+public class BitmapLoadTask extends AsyncTask<Void, Void, BitmapLoadTask.BitmapWorkerResult> {
+
+    private static final String TAG = "BitmapWorkerTask";
+
+    private final Context mContext;
+    private Uri mInputUri;
+    private Uri mOutputUri;
+    private final int mRequiredWidth;
+    private final int mRequiredHeight;
+
+    private final BitmapLoadCallback mBitmapLoadCallback;
+
+    public static class BitmapWorkerResult {
+
+        Bitmap mBitmapResult;
+        ExifInfo mExifInfo;
+        Exception mBitmapWorkerException;
+
+        public BitmapWorkerResult(@NonNull Bitmap bitmapResult, @NonNull ExifInfo exifInfo) {
+            mBitmapResult = bitmapResult;
+            mExifInfo = exifInfo;
+        }
+
+        public BitmapWorkerResult(@NonNull Exception bitmapWorkerException) {
+            mBitmapWorkerException = bitmapWorkerException;
+        }
+
+    }
+
+    public BitmapLoadTask(@NonNull Context context,
+                          @NonNull Uri inputUri, @Nullable Uri outputUri,
+                          int requiredWidth, int requiredHeight,
+                          BitmapLoadCallback loadCallback) {
+        mContext = context;
+        mInputUri = inputUri;
+        mOutputUri = outputUri;
+        mRequiredWidth = requiredWidth;
+        mRequiredHeight = requiredHeight;
+        mBitmapLoadCallback = loadCallback;
+    }
+
+    @Override
+    @NonNull
+    protected BitmapWorkerResult doInBackground(Void... params) {
+        if (mInputUri == null) {
+            return new BitmapWorkerResult(new NullPointerException("Input Uri cannot be null"));
+        }
+
+        try {
+            processInputUri();
+        } catch (NullPointerException | IOException e) {
+            return new BitmapWorkerResult(e);
+        }
+
+        final ParcelFileDescriptor parcelFileDescriptor;
+        try {
+            parcelFileDescriptor = mContext.getContentResolver().openFileDescriptor(mInputUri, "r");
+        } catch (FileNotFoundException e) {
+            return new BitmapWorkerResult(e);
+        }
+
+        final FileDescriptor fileDescriptor;
+        if (parcelFileDescriptor != null) {
+            fileDescriptor = parcelFileDescriptor.getFileDescriptor();
+        } else {
+            return new BitmapWorkerResult(new NullPointerException("ParcelFileDescriptor was null for given Uri: [" + mInputUri + "]"));
+        }
+
+        final BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inJustDecodeBounds = true;
+        BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
+        if (options.outWidth == -1 || options.outHeight == -1) {
+            return new BitmapWorkerResult(new IllegalArgumentException("Bounds for bitmap could not be retrieved from the Uri: [" + mInputUri + "]"));
+        }
+
+        options.inSampleSize = BitmapLoadUtils.calculateInSampleSize(options, mRequiredWidth, mRequiredHeight);
+        options.inJustDecodeBounds = false;
+
+        Bitmap decodeSampledBitmap = null;
+
+        boolean decodeAttemptSuccess = false;
+        while (!decodeAttemptSuccess) {
+            try {
+                decodeSampledBitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
+                decodeAttemptSuccess = true;
+            } catch (OutOfMemoryError error) {
+                Log.e(TAG, "doInBackground: BitmapFactory.decodeFileDescriptor: ", error);
+                options.inSampleSize *= 2;
+            }
+        }
+
+        if (decodeSampledBitmap == null) {
+            return new BitmapWorkerResult(new IllegalArgumentException("Bitmap could not be decoded from the Uri: [" + mInputUri + "]"));
+        }
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+            BitmapLoadUtils.close(parcelFileDescriptor);
+        }
+
+        int exifOrientation = BitmapLoadUtils.getExifOrientation(mContext, mInputUri);
+        int exifDegrees = BitmapLoadUtils.exifToDegrees(exifOrientation);
+        int exifTranslation = BitmapLoadUtils.exifToTranslation(exifOrientation);
+
+        ExifInfo exifInfo = new ExifInfo(exifOrientation, exifDegrees, exifTranslation);
+
+        Matrix matrix = new Matrix();
+        if (exifDegrees != 0) {
+            matrix.preRotate(exifDegrees);
+        }
+        if (exifTranslation != 1) {
+            matrix.postScale(exifTranslation, 1);
+        }
+        if (!matrix.isIdentity()) {
+            return new BitmapWorkerResult(BitmapLoadUtils.transformBitmap(decodeSampledBitmap, matrix), exifInfo);
+        }
+
+        return new BitmapWorkerResult(decodeSampledBitmap, exifInfo);
+    }
+
+    private void processInputUri() throws NullPointerException, IOException {
+        String inputUriScheme = mInputUri.getScheme();
+        Log.d(TAG, "Uri scheme: " + inputUriScheme);
+        if ("http".equals(inputUriScheme) || "https".equals(inputUriScheme)) {
+            try {
+                downloadFile(mInputUri, mOutputUri);
+            } catch (NullPointerException | IOException e) {
+                Log.e(TAG, "Downloading failed", e);
+                throw e;
+            }
+        } else if ("content".equals(inputUriScheme)) {
+            String path = getFilePath();
+            if (!TextUtils.isEmpty(path) && new File(path).exists()) {
+                mInputUri = Uri.fromFile(new File(path));
+            } else {
+                try {
+                    copyFile(mInputUri, mOutputUri);
+                } catch (NullPointerException | IOException e) {
+                    Log.e(TAG, "Copying failed", e);
+                    throw e;
+                }
+            }
+        } else if (!"file".equals(inputUriScheme)) {
+            Log.e(TAG, "Invalid Uri scheme " + inputUriScheme);
+            throw new IllegalArgumentException("Invalid Uri scheme" + inputUriScheme);
+        }
+    }
+
+    private String getFilePath() {
+        if (ContextCompat.checkSelfPermission(mContext, permission.READ_EXTERNAL_STORAGE)
+                == PackageManager.PERMISSION_GRANTED) {
+            return FileUtils.getPath(mContext, mInputUri);
+        } else {
+            return null;
+        }
+    }
+
+    private void copyFile(@NonNull Uri inputUri, @Nullable Uri outputUri) throws NullPointerException, IOException {
+        Log.d(TAG, "copyFile");
+
+        if (outputUri == null) {
+            throw new NullPointerException("Output Uri is null - cannot copy image");
+        }
+
+        InputStream inputStream = null;
+        OutputStream outputStream = null;
+        try {
+            inputStream = mContext.getContentResolver().openInputStream(inputUri);
+            outputStream = new FileOutputStream(new File(outputUri.getPath()));
+            if (inputStream == null) {
+                throw new NullPointerException("InputStream for given input Uri is null");
+            }
+
+            byte buffer[] = new byte[1024];
+            int length;
+            while ((length = inputStream.read(buffer)) > 0) {
+                outputStream.write(buffer, 0, length);
+            }
+        } finally {
+            BitmapLoadUtils.close(outputStream);
+            BitmapLoadUtils.close(inputStream);
+
+            // swap uris, because input image was copied to the output destination
+            // (cropped image will override it later)
+            mInputUri = mOutputUri;
+        }
+    }
+
+    private void downloadFile(@NonNull Uri inputUri, @Nullable Uri outputUri) throws NullPointerException, IOException {
+        Log.d(TAG, "downloadFile");
+
+        if (outputUri == null) {
+            throw new NullPointerException("Output Uri is null - cannot download image");
+        }
+
+        OkHttpClient client = new OkHttpClient();
+
+        BufferedSource source = null;
+        Sink sink = null;
+        Response response = null;
+        try {
+            Request request = new Request.Builder()
+                    .url(inputUri.toString())
+                    .build();
+            response = client.newCall(request).execute();
+            source = response.body().source();
+
+            OutputStream outputStream = mContext.getContentResolver().openOutputStream(outputUri);
+            if (outputStream != null) {
+                sink = Okio.sink(outputStream);
+                source.readAll(sink);
+            } else {
+                throw new NullPointerException("OutputStream for given output Uri is null");
+            }
+        } finally {
+            BitmapLoadUtils.close(source);
+            BitmapLoadUtils.close(sink);
+            if (response != null) {
+                BitmapLoadUtils.close(response.body());
+            }
+            client.dispatcher().cancelAll();
+
+            // swap uris, because input image was downloaded to the output destination
+            // (cropped image will override it later)
+            mInputUri = mOutputUri;
+        }
+    }
+
+    @Override
+    protected void onPostExecute(@NonNull BitmapWorkerResult result) {
+        if (result.mBitmapWorkerException == null) {
+            mBitmapLoadCallback.onBitmapLoaded(result.mBitmapResult, result.mExifInfo, mInputUri.getPath(), (mOutputUri == null) ? null : mOutputUri.getPath());
+        } else {
+            mBitmapLoadCallback.onFailure(result.mBitmapWorkerException);
+        }
+    }
+
+}

+ 173 - 0
ucrop/src/main/java/com/yalantis/ucrop/util/BitmapLoadUtils.java

@@ -0,0 +1,173 @@
+package com.yalantis.ucrop.util;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import android.view.Display;
+import android.view.WindowManager;
+
+import com.yalantis.ucrop.callback.BitmapLoadCallback;
+import com.yalantis.ucrop.task.BitmapLoadTask;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ */
+public class BitmapLoadUtils {
+
+    private static final String TAG = "BitmapLoadUtils";
+
+    public static void decodeBitmapInBackground(@NonNull Context context,
+                                                @NonNull Uri uri, @Nullable Uri outputUri,
+                                                int requiredWidth, int requiredHeight,
+                                                BitmapLoadCallback loadCallback) {
+
+        new BitmapLoadTask(context, uri, outputUri, requiredWidth, requiredHeight, loadCallback).execute();
+    }
+
+    public static Bitmap transformBitmap(@NonNull Bitmap bitmap, @NonNull Matrix transformMatrix) {
+        try {
+            Bitmap converted = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), transformMatrix, true);
+            if (!bitmap.sameAs(converted)) {
+                bitmap = converted;
+            }
+        } catch (OutOfMemoryError error) {
+            Log.e(TAG, "transformBitmap: ", error);
+        }
+        return bitmap;
+    }
+
+    public static int calculateInSampleSize(@NonNull BitmapFactory.Options options, int reqWidth, int reqHeight) {
+        // Raw height and width of image
+        final int height = options.outHeight;
+        final int width = options.outWidth;
+        int inSampleSize = 1;
+
+        if (height > reqHeight || width > reqWidth) {
+            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
+            // height and width lower or equal to the requested height and width.
+            while ((height / inSampleSize) > reqHeight || (width / inSampleSize) > reqWidth) {
+                inSampleSize *= 2;
+            }
+        }
+        return inSampleSize;
+    }
+
+    public static int getExifOrientation(@NonNull Context context, @NonNull Uri imageUri) {
+        int orientation = ExifInterface.ORIENTATION_UNDEFINED;
+        try {
+            InputStream stream = context.getContentResolver().openInputStream(imageUri);
+            if (stream == null) {
+                return orientation;
+            }
+            orientation = new ImageHeaderParser(stream).getOrientation();
+            close(stream);
+        } catch (IOException e) {
+            Log.e(TAG, "getExifOrientation: " + imageUri.toString(), e);
+        }
+        return orientation;
+    }
+
+    public static int exifToDegrees(int exifOrientation) {
+        int rotation;
+        switch (exifOrientation) {
+            case ExifInterface.ORIENTATION_ROTATE_90:
+            case ExifInterface.ORIENTATION_TRANSPOSE:
+                rotation = 90;
+                break;
+            case ExifInterface.ORIENTATION_ROTATE_180:
+            case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+                rotation = 180;
+                break;
+            case ExifInterface.ORIENTATION_ROTATE_270:
+            case ExifInterface.ORIENTATION_TRANSVERSE:
+                rotation = 270;
+                break;
+            default:
+                rotation = 0;
+        }
+        return rotation;
+    }
+
+    public static int exifToTranslation(int exifOrientation) {
+        int translation;
+        switch (exifOrientation) {
+            case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
+            case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+            case ExifInterface.ORIENTATION_TRANSPOSE:
+            case ExifInterface.ORIENTATION_TRANSVERSE:
+                translation = -1;
+                break;
+            default:
+                translation = 1;
+        }
+        return translation;
+    }
+
+    /**
+     * This method calculates maximum size of both width and height of bitmap.
+     * It is twice the device screen diagonal for default implementation (extra quality to zoom image).
+     * Size cannot exceed max texture size.
+     *
+     * @return - max bitmap size in pixels.
+     */
+    @SuppressWarnings({"SuspiciousNameCombination", "deprecation"})
+    public static int calculateMaxBitmapSize(@NonNull Context context) {
+        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+        Display display = wm.getDefaultDisplay();
+
+        Point size = new Point();
+        int width, height;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
+            display.getSize(size);
+            width = size.x;
+            height = size.y;
+        } else {
+            width = display.getWidth();
+            height = display.getHeight();
+        }
+
+        // Twice the device screen diagonal as default
+        int maxBitmapSize = (int) Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
+
+        // Check for max texture size via Canvas
+        Canvas canvas = new Canvas();
+        final int maxCanvasSize = Math.min(canvas.getMaximumBitmapWidth(), canvas.getMaximumBitmapHeight());
+        if (maxCanvasSize > 0) {
+            maxBitmapSize = Math.min(maxBitmapSize, maxCanvasSize);
+        }
+
+        // Check for max texture size via GL
+        final int maxTextureSize = EglUtils.getMaxTextureSize();
+        if (maxTextureSize > 0) {
+            maxBitmapSize = Math.min(maxBitmapSize, maxTextureSize);
+        }
+
+        Log.d(TAG, "maxBitmapSize: " + maxBitmapSize);
+        return maxBitmapSize;
+    }
+
+    @SuppressWarnings("ConstantConditions")
+    public static void close(@Nullable Closeable c) {
+        if (c != null && c instanceof Closeable) { // java.lang.IncompatibleClassChangeError: interface not implemented
+            try {
+                c.close();
+            } catch (IOException e) {
+                // silence
+            }
+        }
+    }
+
+}

+ 17 - 0
ucrop/src/main/java/com/yalantis/ucrop/util/CubicEasing.java

@@ -0,0 +1,17 @@
+package com.yalantis.ucrop.util;
+
+public final class CubicEasing {
+
+    public static float easeOut(float time, float start, float end, float duration) {
+        return end * ((time = time / duration - 1.0f) * time * time + 1.0f) + start;
+    }
+
+    public static float easeIn(float time, float start, float end, float duration) {
+        return end * (time /= duration) * time * time + start;
+    }
+
+    public static float easeInOut(float time, float start, float end, float duration) {
+        return (time /= duration / 2.0f) < 1.0f ? end / 2.0f * time * time * time + start : end / 2.0f * ((time -= 2.0f) * time * time + 2.0f) + start;
+    }
+
+}

+ 134 - 0
ucrop/src/main/java/com/yalantis/ucrop/util/EglUtils.java

@@ -0,0 +1,134 @@
+package com.yalantis.ucrop.util;
+
+import android.annotation.TargetApi;
+import android.opengl.EGL14;
+import android.opengl.EGLConfig;
+import android.opengl.EGLContext;
+import android.opengl.EGLDisplay;
+import android.opengl.EGLSurface;
+import android.opengl.GLES10;
+import android.opengl.GLES20;
+import android.os.Build;
+import android.util.Log;
+
+import javax.microedition.khronos.egl.EGL10;
+
+/**
+ * Created by Oleksii Shliama [https://github.com/shliama] on 9/8/16.
+ */
+public class EglUtils {
+
+    private static final String TAG = "EglUtils";
+
+    private EglUtils() {
+
+    }
+
+    public static int getMaxTextureSize() {
+        try {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+                return getMaxTextureEgl14();
+            } else {
+                return getMaxTextureEgl10();
+            }
+        } catch (Exception e) {
+            Log.d(TAG, "getMaxTextureSize: ", e);
+            return 0;
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+    private static int getMaxTextureEgl14() {
+        EGLDisplay dpy = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
+        int[] vers = new int[2];
+        EGL14.eglInitialize(dpy, vers, 0, vers, 1);
+
+        int[] configAttr = {
+                EGL14.EGL_COLOR_BUFFER_TYPE, EGL14.EGL_RGB_BUFFER,
+                EGL14.EGL_LEVEL, 0,
+                EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
+                EGL14.EGL_SURFACE_TYPE, EGL14.EGL_PBUFFER_BIT,
+                EGL14.EGL_NONE
+        };
+        EGLConfig[] configs = new EGLConfig[1];
+        int[] numConfig = new int[1];
+        EGL14.eglChooseConfig(dpy, configAttr, 0,
+                configs, 0, 1, numConfig, 0);
+        if (numConfig[0] == 0) {
+            return 0;
+        }
+        EGLConfig config = configs[0];
+
+        int[] surfAttr = {
+                EGL14.EGL_WIDTH, 64,
+                EGL14.EGL_HEIGHT, 64,
+                EGL14.EGL_NONE
+        };
+        EGLSurface surf = EGL14.eglCreatePbufferSurface(dpy, config, surfAttr, 0);
+
+        int[] ctxAttrib = {
+                EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
+                EGL14.EGL_NONE
+        };
+        EGLContext ctx = EGL14.eglCreateContext(dpy, config, EGL14.EGL_NO_CONTEXT, ctxAttrib, 0);
+
+        EGL14.eglMakeCurrent(dpy, surf, surf, ctx);
+
+        int[] maxSize = new int[1];
+        GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxSize, 0);
+
+        EGL14.eglMakeCurrent(dpy, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
+                EGL14.EGL_NO_CONTEXT);
+        EGL14.eglDestroySurface(dpy, surf);
+        EGL14.eglDestroyContext(dpy, ctx);
+        EGL14.eglTerminate(dpy);
+
+        return maxSize[0];
+    }
+
+    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+    private static int getMaxTextureEgl10() {
+        EGL10 egl = (EGL10) javax.microedition.khronos.egl.EGLContext.getEGL();
+
+        javax.microedition.khronos.egl.EGLDisplay dpy = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+        int[] vers = new int[2];
+        egl.eglInitialize(dpy, vers);
+
+        int[] configAttr = {
+                EGL10.EGL_COLOR_BUFFER_TYPE, EGL10.EGL_RGB_BUFFER,
+                EGL10.EGL_LEVEL, 0,
+                EGL10.EGL_SURFACE_TYPE, EGL10.EGL_PBUFFER_BIT,
+                EGL10.EGL_NONE
+        };
+        javax.microedition.khronos.egl.EGLConfig[] configs = new javax.microedition.khronos.egl.EGLConfig[1];
+        int[] numConfig = new int[1];
+        egl.eglChooseConfig(dpy, configAttr, configs, 1, numConfig);
+        if (numConfig[0] == 0) {
+            return 0;
+        }
+        javax.microedition.khronos.egl.EGLConfig config = configs[0];
+
+        int[] surfAttr = {
+                EGL10.EGL_WIDTH, 64,
+                EGL10.EGL_HEIGHT, 64,
+                EGL10.EGL_NONE
+        };
+        javax.microedition.khronos.egl.EGLSurface surf = egl.eglCreatePbufferSurface(dpy, config, surfAttr);
+        final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;  // missing in EGL10
+        int[] ctxAttrib = {
+                EGL_CONTEXT_CLIENT_VERSION, 1,
+                EGL10.EGL_NONE
+        };
+        javax.microedition.khronos.egl.EGLContext ctx = egl.eglCreateContext(dpy, config, EGL10.EGL_NO_CONTEXT, ctxAttrib);
+        egl.eglMakeCurrent(dpy, surf, surf, ctx);
+        int[] maxSize = new int[1];
+        GLES10.glGetIntegerv(GLES10.GL_MAX_TEXTURE_SIZE, maxSize, 0);
+        egl.eglMakeCurrent(dpy, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE,
+                EGL10.EGL_NO_CONTEXT);
+        egl.eglDestroySurface(dpy, surf);
+        egl.eglDestroyContext(dpy, ctx);
+        egl.eglTerminate(dpy);
+
+        return maxSize[0];
+    }
+}

+ 103 - 0
ucrop/src/main/java/com/yalantis/ucrop/util/FastBitmapDrawable.java

@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.yalantis.ucrop.util;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+
+public class FastBitmapDrawable extends Drawable {
+
+    private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
+
+    private Bitmap mBitmap;
+    private int mAlpha;
+    private int mWidth, mHeight;
+
+    public FastBitmapDrawable(Bitmap b) {
+        mAlpha = 255;
+        setBitmap(b);
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        if (mBitmap != null && !mBitmap.isRecycled()) {
+            canvas.drawBitmap(mBitmap, null, getBounds(), mPaint);
+        }
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter cf) {
+        mPaint.setColorFilter(cf);
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSLUCENT;
+    }
+
+    public void setFilterBitmap(boolean filterBitmap) {
+        mPaint.setFilterBitmap(filterBitmap);
+    }
+
+    public int getAlpha() {
+        return mAlpha;
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        mAlpha = alpha;
+        mPaint.setAlpha(alpha);
+    }
+
+    @Override
+    public int getIntrinsicWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public int getIntrinsicHeight() {
+        return mHeight;
+    }
+
+    @Override
+    public int getMinimumWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public int getMinimumHeight() {
+        return mHeight;
+    }
+
+    public Bitmap getBitmap() {
+        return mBitmap;
+    }
+
+    public void setBitmap(Bitmap b) {
+        mBitmap = b;
+        if (b != null) {
+            mWidth = mBitmap.getWidth();
+            mHeight = mBitmap.getHeight();
+        } else {
+            mWidth = mHeight = 0;
+        }
+    }
+
+}

+ 236 - 0
ucrop/src/main/java/com/yalantis/ucrop/util/FileUtils.java

@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2007-2008 OpenIntents.org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.yalantis.ucrop.util;
+
+import android.annotation.SuppressLint;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.DocumentsContract;
+import android.provider.MediaStore;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.util.Locale;
+
+/**
+ * @author Peli
+ * @author paulburke (ipaulpro)
+ * @version 2013-12-11
+ */
+public class FileUtils {
+
+    /**
+     * TAG for log messages.
+     */
+    static final String TAG = "FileUtils";
+
+    private FileUtils() {
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is ExternalStorageProvider.
+     * @author paulburke
+     */
+    public static boolean isExternalStorageDocument(Uri uri) {
+        return "com.android.externalstorage.documents".equals(uri.getAuthority());
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is DownloadsProvider.
+     * @author paulburke
+     */
+    public static boolean isDownloadsDocument(Uri uri) {
+        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is MediaProvider.
+     * @author paulburke
+     */
+    public static boolean isMediaDocument(Uri uri) {
+        return "com.android.providers.media.documents".equals(uri.getAuthority());
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is Google Photos.
+     */
+    public static boolean isGooglePhotosUri(Uri uri) {
+        return "com.google.android.apps.photos.content".equals(uri.getAuthority());
+    }
+
+    /**
+     * Get the value of the data column for this Uri. This is useful for
+     * MediaStore Uris, and other file-based ContentProviders.
+     *
+     * @param context       The context.
+     * @param uri           The Uri to query.
+     * @param selection     (Optional) Filter used in the query.
+     * @param selectionArgs (Optional) Selection arguments used in the query.
+     * @return The value of the _data column, which is typically a file path.
+     * @author paulburke
+     */
+    public static String getDataColumn(Context context, Uri uri, String selection,
+                                       String[] selectionArgs) {
+
+        Cursor cursor = null;
+        final String column = "_data";
+        final String[] projection = {
+                column
+        };
+
+        try {
+            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
+                    null);
+            if (cursor != null && cursor.moveToFirst()) {
+                final int column_index = cursor.getColumnIndexOrThrow(column);
+                return cursor.getString(column_index);
+            }
+        } catch (IllegalArgumentException ex) {
+            Log.i(TAG, String.format(Locale.getDefault(), "getDataColumn: _data - [%s]", ex.getMessage()));
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get a file path from a Uri. This will get the the path for Storage Access
+     * Framework Documents, as well as the _data field for the MediaStore and
+     * other file-based ContentProviders.<br>
+     * <br>
+     * Callers should check whether the path is local before assuming it
+     * represents a local file.
+     *
+     * @param context The context.
+     * @param uri     The Uri to query.
+     * @author paulburke
+     */
+    @SuppressLint("NewApi")
+    public static String getPath(final Context context, final Uri uri) {
+        final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
+
+        // DocumentProvider
+        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
+            if (isExternalStorageDocument(uri)) {
+                final String docId = DocumentsContract.getDocumentId(uri);
+                final String[] split = docId.split(":");
+                final String type = split[0];
+
+                if ("primary".equalsIgnoreCase(type)) {
+                    return Environment.getExternalStorageDirectory() + "/" + split[1];
+                }
+
+                // TODO handle non-primary volumes
+            }
+            // DownloadsProvider
+            else if (isDownloadsDocument(uri)) {
+
+                final String id = DocumentsContract.getDocumentId(uri);
+                if (!TextUtils.isEmpty(id)) {
+                    try {
+                        final Uri contentUri = ContentUris.withAppendedId(
+                                Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
+                        return getDataColumn(context, contentUri, null, null);
+                    } catch (NumberFormatException e) {
+                        Log.i(TAG, e.getMessage());
+                        return null;
+                    }
+                }
+
+            }
+            // MediaProvider
+            else if (isMediaDocument(uri)) {
+                final String docId = DocumentsContract.getDocumentId(uri);
+                final String[] split = docId.split(":");
+                final String type = split[0];
+
+                Uri contentUri = null;
+                if ("image".equals(type)) {
+                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+                } else if ("video".equals(type)) {
+                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+                } else if ("audio".equals(type)) {
+                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+                }
+
+                final String selection = "_id=?";
+                final String[] selectionArgs = new String[]{
+                        split[1]
+                };
+
+                return getDataColumn(context, contentUri, selection, selectionArgs);
+            }
+        }
+        // MediaStore (and general)
+        else if ("content".equalsIgnoreCase(uri.getScheme())) {
+
+            // Return the remote address
+            if (isGooglePhotosUri(uri)) {
+                return uri.getLastPathSegment();
+            }
+
+            return getDataColumn(context, uri, null, null);
+        }
+        // File
+        else if ("file".equalsIgnoreCase(uri.getScheme())) {
+            return uri.getPath();
+        }
+
+        return null;
+    }
+
+    /**
+     * Copies one file into the other with the given paths.
+     * In the event that the paths are the same, trying to copy one file to the other
+     * will cause both files to become null.
+     * Simply skipping this step if the paths are identical.
+     */
+    public static void copyFile(@NonNull String pathFrom, @NonNull String pathTo) throws IOException {
+        if (pathFrom.equalsIgnoreCase(pathTo)) {
+            return;
+        }
+
+        FileChannel outputChannel = null;
+        FileChannel inputChannel = null;
+        try {
+            inputChannel = new FileInputStream(new File(pathFrom)).getChannel();
+            outputChannel = new FileOutputStream(new File(pathTo)).getChannel();
+            inputChannel.transferTo(0, inputChannel.size(), outputChannel);
+            inputChannel.close();
+        } finally {
+            if (inputChannel != null) inputChannel.close();
+            if (outputChannel != null) outputChannel.close();
+        }
+    }
+
+}

+ 425 - 0
ucrop/src/main/java/com/yalantis/ucrop/util/ImageHeaderParser.java

@@ -0,0 +1,425 @@
+/*
+ * Copyright 2015 Google, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of
+ * conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list
+ * of conditions and the following disclaimer in the documentation and/or other materials
+ * provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY GOOGLE, INC. ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of Google, Inc.
+ *
+ * Adapted for the uCrop library.
+ */
+
+package com.yalantis.ucrop.util;
+
+import android.media.ExifInterface;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+
+/**
+ * A class for parsing the exif orientation from an image header.
+ */
+public class ImageHeaderParser {
+    private static final String TAG = "ImageHeaderParser";
+    /**
+     * A constant indicating we were unable to parse the orientation from the image either because
+     * no exif segment containing orientation data existed, or because of an I/O error attempting to
+     * read the exif segment.
+     */
+    public static final int UNKNOWN_ORIENTATION = -1;
+
+    private static final int EXIF_MAGIC_NUMBER = 0xFFD8;
+    // "MM".
+    private static final int MOTOROLA_TIFF_MAGIC_NUMBER = 0x4D4D;
+    // "II".
+    private static final int INTEL_TIFF_MAGIC_NUMBER = 0x4949;
+    private static final String JPEG_EXIF_SEGMENT_PREAMBLE = "Exif\0\0";
+    private static final byte[] JPEG_EXIF_SEGMENT_PREAMBLE_BYTES =
+            JPEG_EXIF_SEGMENT_PREAMBLE.getBytes(Charset.forName("UTF-8"));
+    private static final int SEGMENT_SOS = 0xDA;
+    private static final int MARKER_EOI = 0xD9;
+    private static final int SEGMENT_START_ID = 0xFF;
+    private static final int EXIF_SEGMENT_TYPE = 0xE1;
+    private static final int ORIENTATION_TAG_TYPE = 0x0112;
+    private static final int[] BYTES_PER_FORMAT = {0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8};
+
+    private final Reader reader;
+
+    public ImageHeaderParser(InputStream is) {
+        reader = new StreamReader(is);
+    }
+
+    /**
+     * Parse the orientation from the image header. If it doesn't handle this image type (or this is
+     * not an image) it will return a default value rather than throwing an exception.
+     *
+     * @return The exif orientation if present or -1 if the header couldn't be parsed or doesn't
+     * contain an orientation
+     * @throws IOException
+     */
+    public int getOrientation() throws IOException {
+        final int magicNumber = reader.getUInt16();
+
+        if (!handles(magicNumber)) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Parser doesn't handle magic number: " + magicNumber);
+            }
+            return UNKNOWN_ORIENTATION;
+        } else {
+            int exifSegmentLength = moveToExifSegmentAndGetLength();
+            if (exifSegmentLength == -1) {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Failed to parse exif segment length, or exif segment not found");
+                }
+                return UNKNOWN_ORIENTATION;
+            }
+
+            byte[] exifData = new byte[exifSegmentLength];
+            return parseExifSegment(exifData, exifSegmentLength);
+        }
+    }
+
+    private int parseExifSegment(byte[] tempArray, int exifSegmentLength) throws IOException {
+        int read = reader.read(tempArray, exifSegmentLength);
+        if (read != exifSegmentLength) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Unable to read exif segment data"
+                        + ", length: " + exifSegmentLength
+                        + ", actually read: " + read);
+            }
+            return UNKNOWN_ORIENTATION;
+        }
+
+        boolean hasJpegExifPreamble = hasJpegExifPreamble(tempArray, exifSegmentLength);
+        if (hasJpegExifPreamble) {
+            return parseExifSegment(new RandomAccessReader(tempArray, exifSegmentLength));
+        } else {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Missing jpeg exif preamble");
+            }
+            return UNKNOWN_ORIENTATION;
+        }
+    }
+
+    private boolean hasJpegExifPreamble(byte[] exifData, int exifSegmentLength) {
+        boolean result =
+                exifData != null && exifSegmentLength > JPEG_EXIF_SEGMENT_PREAMBLE_BYTES.length;
+        if (result) {
+            for (int i = 0; i < JPEG_EXIF_SEGMENT_PREAMBLE_BYTES.length; i++) {
+                if (exifData[i] != JPEG_EXIF_SEGMENT_PREAMBLE_BYTES[i]) {
+                    result = false;
+                    break;
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Moves reader to the start of the exif segment and returns the length of the exif segment or
+     * {@code -1} if no exif segment is found.
+     */
+    private int moveToExifSegmentAndGetLength() throws IOException {
+        short segmentId, segmentType;
+        int segmentLength;
+        while (true) {
+            segmentId = reader.getUInt8();
+            if (segmentId != SEGMENT_START_ID) {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Unknown segmentId=" + segmentId);
+                }
+                return -1;
+            }
+
+            segmentType = reader.getUInt8();
+
+            if (segmentType == SEGMENT_SOS) {
+                return -1;
+            } else if (segmentType == MARKER_EOI) {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Found MARKER_EOI in exif segment");
+                }
+                return -1;
+            }
+
+            // Segment length includes bytes for segment length.
+            segmentLength = reader.getUInt16() - 2;
+
+            if (segmentType != EXIF_SEGMENT_TYPE) {
+                long skipped = reader.skip(segmentLength);
+                if (skipped != segmentLength) {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Unable to skip enough data"
+                                + ", type: " + segmentType
+                                + ", wanted to skip: " + segmentLength
+                                + ", but actually skipped: " + skipped);
+                    }
+                    return -1;
+                }
+            } else {
+                return segmentLength;
+            }
+        }
+    }
+
+    private static int parseExifSegment(RandomAccessReader segmentData) {
+        final int headerOffsetSize = JPEG_EXIF_SEGMENT_PREAMBLE.length();
+
+        short byteOrderIdentifier = segmentData.getInt16(headerOffsetSize);
+        final ByteOrder byteOrder;
+        if (byteOrderIdentifier == MOTOROLA_TIFF_MAGIC_NUMBER) {
+            byteOrder = ByteOrder.BIG_ENDIAN;
+        } else if (byteOrderIdentifier == INTEL_TIFF_MAGIC_NUMBER) {
+            byteOrder = ByteOrder.LITTLE_ENDIAN;
+        } else {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Unknown endianness = " + byteOrderIdentifier);
+            }
+            byteOrder = ByteOrder.BIG_ENDIAN;
+        }
+
+        segmentData.order(byteOrder);
+
+        int firstIfdOffset = segmentData.getInt32(headerOffsetSize + 4) + headerOffsetSize;
+        int tagCount = segmentData.getInt16(firstIfdOffset);
+
+        int tagOffset, tagType, formatCode, componentCount;
+        for (int i = 0; i < tagCount; i++) {
+            tagOffset = calcTagOffset(firstIfdOffset, i);
+            tagType = segmentData.getInt16(tagOffset);
+
+            // We only want orientation.
+            if (tagType != ORIENTATION_TAG_TYPE) {
+                continue;
+            }
+
+            formatCode = segmentData.getInt16(tagOffset + 2);
+
+            // 12 is max format code.
+            if (formatCode < 1 || formatCode > 12) {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Got invalid format code = " + formatCode);
+                }
+                continue;
+            }
+
+            componentCount = segmentData.getInt32(tagOffset + 4);
+
+            if (componentCount < 0) {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Negative tiff component count");
+                }
+                continue;
+            }
+
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Got tagIndex=" + i + " tagType=" + tagType + " formatCode=" + formatCode
+                        + " componentCount=" + componentCount);
+            }
+
+            final int byteCount = componentCount + BYTES_PER_FORMAT[formatCode];
+
+            if (byteCount > 4) {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Got byte count > 4, not orientation, continuing, formatCode=" + formatCode);
+                }
+                continue;
+            }
+
+            final int tagValueOffset = tagOffset + 8;
+
+            if (tagValueOffset < 0 || tagValueOffset > segmentData.length()) {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Illegal tagValueOffset=" + tagValueOffset + " tagType=" + tagType);
+                }
+                continue;
+            }
+
+            if (byteCount < 0 || tagValueOffset + byteCount > segmentData.length()) {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Illegal number of bytes for TI tag data tagType=" + tagType);
+                }
+                continue;
+            }
+
+            //assume componentCount == 1 && fmtCode == 3
+            return segmentData.getInt16(tagValueOffset);
+        }
+
+        return -1;
+    }
+
+    private static int calcTagOffset(int ifdOffset, int tagIndex) {
+        return ifdOffset + 2 + 12 * tagIndex;
+    }
+
+    private static boolean handles(int imageMagicNumber) {
+        return (imageMagicNumber & EXIF_MAGIC_NUMBER) == EXIF_MAGIC_NUMBER
+                || imageMagicNumber == MOTOROLA_TIFF_MAGIC_NUMBER
+                || imageMagicNumber == INTEL_TIFF_MAGIC_NUMBER;
+    }
+
+    private static class RandomAccessReader {
+        private final ByteBuffer data;
+
+        public RandomAccessReader(byte[] data, int length) {
+            this.data = (ByteBuffer) ByteBuffer.wrap(data)
+                    .order(ByteOrder.BIG_ENDIAN)
+                    .limit(length);
+        }
+
+        public void order(ByteOrder byteOrder) {
+            this.data.order(byteOrder);
+        }
+
+        public int length() {
+            return data.remaining();
+        }
+
+        public int getInt32(int offset) {
+            return data.getInt(offset);
+        }
+
+        public short getInt16(int offset) {
+            return data.getShort(offset);
+        }
+    }
+
+    private interface Reader {
+        int getUInt16() throws IOException;
+
+        short getUInt8() throws IOException;
+
+        long skip(long total) throws IOException;
+
+        int read(byte[] buffer, int byteCount) throws IOException;
+    }
+
+    private static class StreamReader implements Reader {
+        private final InputStream is;
+
+        // Motorola / big endian byte order.
+        public StreamReader(InputStream is) {
+            this.is = is;
+        }
+
+        @Override
+        public int getUInt16() throws IOException {
+            return (is.read() << 8 & 0xFF00) | (is.read() & 0xFF);
+        }
+
+        @Override
+        public short getUInt8() throws IOException {
+            return (short) (is.read() & 0xFF);
+        }
+
+        @Override
+        public long skip(long total) throws IOException {
+            if (total < 0) {
+                return 0;
+            }
+
+            long toSkip = total;
+            while (toSkip > 0) {
+                long skipped = is.skip(toSkip);
+                if (skipped > 0) {
+                    toSkip -= skipped;
+                } else {
+                    // Skip has no specific contract as to what happens when you reach the end of
+                    // the stream. To differentiate between temporarily not having more data and
+                    // having finished the stream, we read a single byte when we fail to skip any
+                    // amount of data.
+                    int testEofByte = is.read();
+                    if (testEofByte == -1) {
+                        break;
+                    } else {
+                        toSkip--;
+                    }
+                }
+            }
+            return total - toSkip;
+        }
+
+        @Override
+        public int read(byte[] buffer, int byteCount) throws IOException {
+            int toRead = byteCount;
+            int read;
+            while (toRead > 0 && ((read = is.read(buffer, byteCount - toRead, toRead)) != -1)) {
+                toRead -= read;
+            }
+            return byteCount - toRead;
+        }
+    }
+
+    public static void copyExif(ExifInterface originalExif, int width, int height, String imageOutputPath) {
+        String[] attributes = new String[]{
+                ExifInterface.TAG_APERTURE,
+                ExifInterface.TAG_DATETIME,
+                ExifInterface.TAG_DATETIME_DIGITIZED,
+                ExifInterface.TAG_EXPOSURE_TIME,
+                ExifInterface.TAG_FLASH,
+                ExifInterface.TAG_FOCAL_LENGTH,
+                ExifInterface.TAG_GPS_ALTITUDE,
+                ExifInterface.TAG_GPS_ALTITUDE_REF,
+                ExifInterface.TAG_GPS_DATESTAMP,
+                ExifInterface.TAG_GPS_LATITUDE,
+                ExifInterface.TAG_GPS_LATITUDE_REF,
+                ExifInterface.TAG_GPS_LONGITUDE,
+                ExifInterface.TAG_GPS_LONGITUDE_REF,
+                ExifInterface.TAG_GPS_PROCESSING_METHOD,
+                ExifInterface.TAG_GPS_TIMESTAMP,
+                ExifInterface.TAG_ISO,
+                ExifInterface.TAG_MAKE,
+                ExifInterface.TAG_MODEL,
+                ExifInterface.TAG_SUBSEC_TIME,
+                ExifInterface.TAG_SUBSEC_TIME_DIG,
+                ExifInterface.TAG_SUBSEC_TIME_ORIG,
+                ExifInterface.TAG_WHITE_BALANCE
+        };
+
+        try {
+            ExifInterface newExif = new ExifInterface(imageOutputPath);
+            String value;
+            for (String attribute : attributes) {
+                value = originalExif.getAttribute(attribute);
+                if (!TextUtils.isEmpty(value)) {
+                    newExif.setAttribute(attribute, value);
+                }
+            }
+            newExif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(width));
+            newExif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(height));
+            newExif.setAttribute(ExifInterface.TAG_ORIENTATION, "0");
+
+            newExif.saveAttributes();
+
+        } catch (IOException e) {
+            Log.d(TAG, e.getMessage());
+        }
+    }
+
+}
+

+ 72 - 0
ucrop/src/main/java/com/yalantis/ucrop/util/RectUtils.java

@@ -0,0 +1,72 @@
+package com.yalantis.ucrop.util;
+
+import android.graphics.RectF;
+
+public class RectUtils {
+
+    /**
+     * Gets a float array of the 2D coordinates representing a rectangles
+     * corners.
+     * The order of the corners in the float array is:
+     * 0------->1
+     * ^        |
+     * |        |
+     * |        v
+     * 3<-------2
+     *
+     * @param r the rectangle to get the corners of
+     * @return the float array of corners (8 floats)
+     */
+    public static float[] getCornersFromRect(RectF r) {
+        return new float[]{
+                r.left, r.top,
+                r.right, r.top,
+                r.right, r.bottom,
+                r.left, r.bottom
+        };
+    }
+
+    /**
+     * Gets a float array of two lengths representing a rectangles width and height
+     * The order of the corners in the input float array is:
+     * 0------->1
+     * ^        |
+     * |        |
+     * |        v
+     * 3<-------2
+     *
+     * @param corners the float array of corners (8 floats)
+     * @return the float array of width and height (2 floats)
+     */
+    public static float[] getRectSidesFromCorners(float[] corners) {
+        return new float[]{(float) Math.sqrt(Math.pow(corners[0] - corners[2], 2) + Math.pow(corners[1] - corners[3], 2)),
+                (float) Math.sqrt(Math.pow(corners[2] - corners[4], 2) + Math.pow(corners[3] - corners[5], 2))};
+    }
+
+    public static float[] getCenterFromRect(RectF r) {
+        return new float[]{r.centerX(), r.centerY()};
+    }
+
+    /**
+     * Takes an array of 2D coordinates representing corners and returns the
+     * smallest rectangle containing those coordinates.
+     *
+     * @param array array of 2D coordinates
+     * @return smallest rectangle containing coordinates
+     */
+    public static RectF trapToRect(float[] array) {
+        RectF r = new RectF(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
+                Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);
+        for (int i = 1; i < array.length; i += 2) {
+            float x = Math.round(array[i - 1] * 10) / 10.f;
+            float y = Math.round(array[i] * 10) / 10.f;
+            r.left = (x < r.left) ? x : r.left;
+            r.top = (y < r.top) ? y : r.top;
+            r.right = (x > r.right) ? x : r.right;
+            r.bottom = (y > r.bottom) ? y : r.bottom;
+        }
+        r.sort();
+        return r;
+    }
+
+}

+ 111 - 0
ucrop/src/main/java/com/yalantis/ucrop/util/RotationGestureDetector.java

@@ -0,0 +1,111 @@
+package com.yalantis.ucrop.util;
+
+import android.support.annotation.NonNull;
+import android.view.MotionEvent;
+
+public class RotationGestureDetector {
+
+    private static final int INVALID_POINTER_INDEX = -1;
+
+    private float fX, fY, sX, sY;
+
+    private int mPointerIndex1, mPointerIndex2;
+    private float mAngle;
+    private boolean mIsFirstTouch;
+
+    private OnRotationGestureListener mListener;
+
+    public RotationGestureDetector(OnRotationGestureListener listener) {
+        mListener = listener;
+        mPointerIndex1 = INVALID_POINTER_INDEX;
+        mPointerIndex2 = INVALID_POINTER_INDEX;
+    }
+
+    public float getAngle() {
+        return mAngle;
+    }
+
+    public boolean onTouchEvent(@NonNull MotionEvent event) {
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                sX = event.getX();
+                sY = event.getY();
+                mPointerIndex1 = event.findPointerIndex(event.getPointerId(0));
+                mAngle = 0;
+                mIsFirstTouch = true;
+                break;
+            case MotionEvent.ACTION_POINTER_DOWN:
+                fX = event.getX();
+                fY = event.getY();
+                mPointerIndex2 = event.findPointerIndex(event.getPointerId(event.getActionIndex()));
+                mAngle = 0;
+                mIsFirstTouch = true;
+                break;
+            case MotionEvent.ACTION_MOVE:
+                if (mPointerIndex1 != INVALID_POINTER_INDEX && mPointerIndex2 != INVALID_POINTER_INDEX && event.getPointerCount() > mPointerIndex2) {
+                    float nfX, nfY, nsX, nsY;
+
+                    nsX = event.getX(mPointerIndex1);
+                    nsY = event.getY(mPointerIndex1);
+                    nfX = event.getX(mPointerIndex2);
+                    nfY = event.getY(mPointerIndex2);
+
+                    if (mIsFirstTouch) {
+                        mAngle = 0;
+                        mIsFirstTouch = false;
+                    } else {
+                        calculateAngleBetweenLines(fX, fY, sX, sY, nfX, nfY, nsX, nsY);
+                    }
+
+                    if (mListener != null) {
+                        mListener.onRotation(this);
+                    }
+                    fX = nfX;
+                    fY = nfY;
+                    sX = nsX;
+                    sY = nsY;
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                mPointerIndex1 = INVALID_POINTER_INDEX;
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+                mPointerIndex2 = INVALID_POINTER_INDEX;
+                break;
+        }
+        return true;
+    }
+
+    private float calculateAngleBetweenLines(float fx1, float fy1, float fx2, float fy2,
+                                             float sx1, float sy1, float sx2, float sy2) {
+        return calculateAngleDelta(
+                (float) Math.toDegrees((float) Math.atan2((fy1 - fy2), (fx1 - fx2))),
+                (float) Math.toDegrees((float) Math.atan2((sy1 - sy2), (sx1 - sx2))));
+    }
+
+    private float calculateAngleDelta(float angleFrom, float angleTo) {
+        mAngle = angleTo % 360.0f - angleFrom % 360.0f;
+
+        if (mAngle < -180.0f) {
+            mAngle += 360.0f;
+        } else if (mAngle > 180.0f) {
+            mAngle -= 360.0f;
+        }
+
+        return mAngle;
+    }
+
+    public static class SimpleOnRotationGestureListener implements OnRotationGestureListener {
+
+        @Override
+        public boolean onRotation(RotationGestureDetector rotationDetector) {
+            return false;
+        }
+    }
+
+    public interface OnRotationGestureListener {
+
+        boolean onRotation(RotationGestureDetector rotationDetector);
+    }
+
+}

+ 42 - 0
ucrop/src/main/java/com/yalantis/ucrop/util/SelectedStateListDrawable.java

@@ -0,0 +1,42 @@
+package com.yalantis.ucrop.util;
+
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.StateListDrawable;
+
+/**
+ * Hack class to properly support state drawable back to Android 1.6
+ */
+public class SelectedStateListDrawable extends StateListDrawable {
+
+    private int mSelectionColor;
+
+    public SelectedStateListDrawable(Drawable drawable, int selectionColor) {
+        super();
+        this.mSelectionColor = selectionColor;
+        addState(new int[]{android.R.attr.state_selected}, drawable);
+        addState(new int[]{}, drawable);
+    }
+
+    @Override
+    protected boolean onStateChange(int[] states) {
+        boolean isStatePressedInArray = false;
+        for (int state : states) {
+            if (state == android.R.attr.state_selected) {
+                isStatePressedInArray = true;
+            }
+        }
+        if (isStatePressedInArray) {
+            super.setColorFilter(mSelectionColor, PorterDuff.Mode.SRC_ATOP);
+        } else {
+            super.clearColorFilter();
+        }
+        return super.onStateChange(states);
+    }
+
+    @Override
+    public boolean isStateful() {
+        return true;
+    }
+
+}

+ 627 - 0
ucrop/src/main/java/com/yalantis/ucrop/view/CropImageView.java

@@ -0,0 +1,627 @@
+package com.yalantis.ucrop.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+
+import com.yalantis.ucrop.R;
+import com.yalantis.ucrop.callback.BitmapCropCallback;
+import com.yalantis.ucrop.callback.CropBoundsChangeListener;
+import com.yalantis.ucrop.model.CropParameters;
+import com.yalantis.ucrop.model.ImageState;
+import com.yalantis.ucrop.task.BitmapCropTask;
+import com.yalantis.ucrop.util.CubicEasing;
+import com.yalantis.ucrop.util.RectUtils;
+
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ * <p/>
+ * This class adds crop feature, methods to draw crop guidelines, and keep image in correct state.
+ * Also it extends parent class methods to add checks for scale; animating zoom in/out.
+ */
+public class CropImageView extends TransformImageView {
+
+    public static final int DEFAULT_MAX_BITMAP_SIZE = 0;
+    public static final int DEFAULT_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION = 500;
+    public static final float DEFAULT_MAX_SCALE_MULTIPLIER = 10.0f;
+    public static final float SOURCE_IMAGE_ASPECT_RATIO = 0f;
+    public static final float DEFAULT_ASPECT_RATIO = SOURCE_IMAGE_ASPECT_RATIO;
+
+    private final RectF mCropRect = new RectF();
+
+    private final Matrix mTempMatrix = new Matrix();
+
+    private float mTargetAspectRatio;
+    private float mMaxScaleMultiplier = DEFAULT_MAX_SCALE_MULTIPLIER;
+
+    private CropBoundsChangeListener mCropBoundsChangeListener;
+
+    private Runnable mWrapCropBoundsRunnable, mZoomImageToPositionRunnable = null;
+
+    private float mMaxScale, mMinScale;
+    private int mMaxResultImageSizeX = 0, mMaxResultImageSizeY = 0;
+    private long mImageToWrapCropBoundsAnimDuration = DEFAULT_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION;
+
+    public CropImageView(Context context) {
+        this(context, null);
+    }
+
+    public CropImageView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public CropImageView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    /**
+     * Cancels all current animations and sets image to fill crop area (without animation).
+     * Then creates and executes {@link BitmapCropTask} with proper parameters.
+     */
+    public void cropAndSaveImage(@NonNull Bitmap.CompressFormat compressFormat, int compressQuality,
+                                 @Nullable BitmapCropCallback cropCallback) {
+        cancelAllAnimations();
+        setImageToWrapCropBounds(false);
+
+        final ImageState imageState = new ImageState(
+                mCropRect, RectUtils.trapToRect(mCurrentImageCorners),
+                getCurrentScale(), getCurrentAngle());
+
+        final CropParameters cropParameters = new CropParameters(
+                mMaxResultImageSizeX, mMaxResultImageSizeY,
+                compressFormat, compressQuality,
+                getImageInputPath(), getImageOutputPath(), getExifInfo());
+
+        new BitmapCropTask(getViewBitmap(), imageState, cropParameters, cropCallback).execute();
+    }
+
+    /**
+     * @return - maximum scale value for current image and crop ratio
+     */
+    public float getMaxScale() {
+        return mMaxScale;
+    }
+
+    /**
+     * @return - minimum scale value for current image and crop ratio
+     */
+    public float getMinScale() {
+        return mMinScale;
+    }
+
+    /**
+     * @return - aspect ratio for crop bounds
+     */
+    public float getTargetAspectRatio() {
+        return mTargetAspectRatio;
+    }
+
+    /**
+     * Updates current crop rectangle with given. Also recalculates image properties and position
+     * to fit new crop rectangle.
+     *
+     * @param cropRect - new crop rectangle
+     */
+    public void setCropRect(RectF cropRect) {
+        mTargetAspectRatio = cropRect.width() / cropRect.height();
+        mCropRect.set(cropRect.left - getPaddingLeft(), cropRect.top - getPaddingTop(),
+                cropRect.right - getPaddingRight(), cropRect.bottom - getPaddingBottom());
+        calculateImageScaleBounds();
+        setImageToWrapCropBounds();
+    }
+
+    /**
+     * This method sets aspect ratio for crop bounds.
+     * If {@link #SOURCE_IMAGE_ASPECT_RATIO} value is passed - aspect ratio is calculated
+     * based on current image width and height.
+     *
+     * @param targetAspectRatio - aspect ratio for image crop (e.g. 1.77(7) for 16:9)
+     */
+    public void setTargetAspectRatio(float targetAspectRatio) {
+        final Drawable drawable = getDrawable();
+        if (drawable == null) {
+            mTargetAspectRatio = targetAspectRatio;
+            return;
+        }
+
+        if (targetAspectRatio == SOURCE_IMAGE_ASPECT_RATIO) {
+            mTargetAspectRatio = drawable.getIntrinsicWidth() / (float) drawable.getIntrinsicHeight();
+        } else {
+            mTargetAspectRatio = targetAspectRatio;
+        }
+
+        if (mCropBoundsChangeListener != null) {
+            mCropBoundsChangeListener.onCropAspectRatioChanged(mTargetAspectRatio);
+        }
+    }
+
+    @Nullable
+    public CropBoundsChangeListener getCropBoundsChangeListener() {
+        return mCropBoundsChangeListener;
+    }
+
+    public void setCropBoundsChangeListener(@Nullable CropBoundsChangeListener cropBoundsChangeListener) {
+        mCropBoundsChangeListener = cropBoundsChangeListener;
+    }
+
+    /**
+     * This method sets maximum width for resulting cropped image
+     *
+     * @param maxResultImageSizeX - size in pixels
+     */
+    public void setMaxResultImageSizeX(@IntRange(from = 10) int maxResultImageSizeX) {
+        mMaxResultImageSizeX = maxResultImageSizeX;
+    }
+
+    /**
+     * This method sets maximum width for resulting cropped image
+     *
+     * @param maxResultImageSizeY - size in pixels
+     */
+    public void setMaxResultImageSizeY(@IntRange(from = 10) int maxResultImageSizeY) {
+        mMaxResultImageSizeY = maxResultImageSizeY;
+    }
+
+    /**
+     * This method sets animation duration for image to wrap the crop bounds
+     *
+     * @param imageToWrapCropBoundsAnimDuration - duration in milliseconds
+     */
+    public void setImageToWrapCropBoundsAnimDuration(@IntRange(from = 100) long imageToWrapCropBoundsAnimDuration) {
+        if (imageToWrapCropBoundsAnimDuration > 0) {
+            mImageToWrapCropBoundsAnimDuration = imageToWrapCropBoundsAnimDuration;
+        } else {
+            throw new IllegalArgumentException("Animation duration cannot be negative value.");
+        }
+    }
+
+    /**
+     * This method sets multiplier that is used to calculate max image scale from min image scale.
+     *
+     * @param maxScaleMultiplier - (minScale * maxScaleMultiplier) = maxScale
+     */
+    public void setMaxScaleMultiplier(float maxScaleMultiplier) {
+        mMaxScaleMultiplier = maxScaleMultiplier;
+    }
+
+    /**
+     * This method scales image down for given value related to image center.
+     */
+    public void zoomOutImage(float deltaScale) {
+        zoomOutImage(deltaScale, mCropRect.centerX(), mCropRect.centerY());
+    }
+
+    /**
+     * This method scales image down for given value related given coords (x, y).
+     */
+    public void zoomOutImage(float scale, float centerX, float centerY) {
+        if (scale >= getMinScale()) {
+            postScale(scale / getCurrentScale(), centerX, centerY);
+        }
+    }
+
+    /**
+     * This method scales image up for given value related to image center.
+     */
+    public void zoomInImage(float deltaScale) {
+        zoomInImage(deltaScale, mCropRect.centerX(), mCropRect.centerY());
+    }
+
+    /**
+     * This method scales image up for given value related to given coords (x, y).
+     */
+    public void zoomInImage(float scale, float centerX, float centerY) {
+        if (scale <= getMaxScale()) {
+            postScale(scale / getCurrentScale(), centerX, centerY);
+        }
+    }
+
+    /**
+     * This method changes image scale for given value related to point (px, py) but only if
+     * resulting scale is in min/max bounds.
+     *
+     * @param deltaScale - scale value
+     * @param px         - scale center X
+     * @param py         - scale center Y
+     */
+    public void postScale(float deltaScale, float px, float py) {
+        if (deltaScale > 1 && getCurrentScale() * deltaScale <= getMaxScale()) {
+            super.postScale(deltaScale, px, py);
+        } else if (deltaScale < 1 && getCurrentScale() * deltaScale >= getMinScale()) {
+            super.postScale(deltaScale, px, py);
+        }
+    }
+
+    /**
+     * This method rotates image for given angle related to the image center.
+     *
+     * @param deltaAngle - angle to rotate
+     */
+    public void postRotate(float deltaAngle) {
+        postRotate(deltaAngle, mCropRect.centerX(), mCropRect.centerY());
+    }
+
+    /**
+     * This method cancels all current Runnable objects that represent animations.
+     */
+    public void cancelAllAnimations() {
+        removeCallbacks(mWrapCropBoundsRunnable);
+        removeCallbacks(mZoomImageToPositionRunnable);
+    }
+
+    public void setImageToWrapCropBounds() {
+        setImageToWrapCropBounds(true);
+    }
+
+    /**
+     * If image doesn't fill the crop bounds it must be translated and scaled properly to fill those.
+     * <p/>
+     * Therefore this method calculates delta X, Y and scale values and passes them to the
+     * {@link WrapCropBoundsRunnable} which animates image.
+     * Scale value must be calculated only if image won't fill the crop bounds after it's translated to the
+     * crop bounds rectangle center. Using temporary variables this method checks this case.
+     */
+    public void setImageToWrapCropBounds(boolean animate) {
+        if (mBitmapLaidOut && !isImageWrapCropBounds()) {
+
+            float currentX = mCurrentImageCenter[0];
+            float currentY = mCurrentImageCenter[1];
+            float currentScale = getCurrentScale();
+
+            float deltaX = mCropRect.centerX() - currentX;
+            float deltaY = mCropRect.centerY() - currentY;
+            float deltaScale = 0;
+
+            mTempMatrix.reset();
+            mTempMatrix.setTranslate(deltaX, deltaY);
+
+            final float[] tempCurrentImageCorners = Arrays.copyOf(mCurrentImageCorners, mCurrentImageCorners.length);
+            mTempMatrix.mapPoints(tempCurrentImageCorners);
+
+            boolean willImageWrapCropBoundsAfterTranslate = isImageWrapCropBounds(tempCurrentImageCorners);
+
+            if (willImageWrapCropBoundsAfterTranslate) {
+                final float[] imageIndents = calculateImageIndents();
+                deltaX = -(imageIndents[0] + imageIndents[2]);
+                deltaY = -(imageIndents[1] + imageIndents[3]);
+            } else {
+                RectF tempCropRect = new RectF(mCropRect);
+                mTempMatrix.reset();
+                mTempMatrix.setRotate(getCurrentAngle());
+                mTempMatrix.mapRect(tempCropRect);
+
+                final float[] currentImageSides = RectUtils.getRectSidesFromCorners(mCurrentImageCorners);
+
+                deltaScale = Math.max(tempCropRect.width() / currentImageSides[0],
+                        tempCropRect.height() / currentImageSides[1]);
+                deltaScale = deltaScale * currentScale - currentScale;
+            }
+
+            if (animate) {
+                post(mWrapCropBoundsRunnable = new WrapCropBoundsRunnable(
+                        CropImageView.this, mImageToWrapCropBoundsAnimDuration, currentX, currentY, deltaX, deltaY,
+                        currentScale, deltaScale, willImageWrapCropBoundsAfterTranslate));
+            } else {
+                postTranslate(deltaX, deltaY);
+                if (!willImageWrapCropBoundsAfterTranslate) {
+                    zoomInImage(currentScale + deltaScale, mCropRect.centerX(), mCropRect.centerY());
+                }
+            }
+        }
+    }
+
+    /**
+     * First, un-rotate image and crop rectangles (make image rectangle axis-aligned).
+     * Second, calculate deltas between those rectangles sides.
+     * Third, depending on delta (its sign) put them or zero inside an array.
+     * Fourth, using Matrix, rotate back those points (indents).
+     *
+     * @return - the float array of image indents (4 floats) - in this order [left, top, right, bottom]
+     */
+    private float[] calculateImageIndents() {
+        mTempMatrix.reset();
+        mTempMatrix.setRotate(-getCurrentAngle());
+
+        float[] unrotatedImageCorners = Arrays.copyOf(mCurrentImageCorners, mCurrentImageCorners.length);
+        float[] unrotatedCropBoundsCorners = RectUtils.getCornersFromRect(mCropRect);
+
+        mTempMatrix.mapPoints(unrotatedImageCorners);
+        mTempMatrix.mapPoints(unrotatedCropBoundsCorners);
+
+        RectF unrotatedImageRect = RectUtils.trapToRect(unrotatedImageCorners);
+        RectF unrotatedCropRect = RectUtils.trapToRect(unrotatedCropBoundsCorners);
+
+        float deltaLeft = unrotatedImageRect.left - unrotatedCropRect.left;
+        float deltaTop = unrotatedImageRect.top - unrotatedCropRect.top;
+        float deltaRight = unrotatedImageRect.right - unrotatedCropRect.right;
+        float deltaBottom = unrotatedImageRect.bottom - unrotatedCropRect.bottom;
+
+        float indents[] = new float[4];
+        indents[0] = (deltaLeft > 0) ? deltaLeft : 0;
+        indents[1] = (deltaTop > 0) ? deltaTop : 0;
+        indents[2] = (deltaRight < 0) ? deltaRight : 0;
+        indents[3] = (deltaBottom < 0) ? deltaBottom : 0;
+
+        mTempMatrix.reset();
+        mTempMatrix.setRotate(getCurrentAngle());
+        mTempMatrix.mapPoints(indents);
+
+        return indents;
+    }
+
+    /**
+     * When image is laid out it must be centered properly to fit current crop bounds.
+     */
+    @Override
+    protected void onImageLaidOut() {
+        super.onImageLaidOut();
+        final Drawable drawable = getDrawable();
+        if (drawable == null) {
+            return;
+        }
+
+        float drawableWidth = drawable.getIntrinsicWidth();
+        float drawableHeight = drawable.getIntrinsicHeight();
+
+        if (mTargetAspectRatio == SOURCE_IMAGE_ASPECT_RATIO) {
+            mTargetAspectRatio = drawableWidth / drawableHeight;
+        }
+
+        int height = (int) (mThisWidth / mTargetAspectRatio);
+        if (height > mThisHeight) {
+            int width = (int) (mThisHeight * mTargetAspectRatio);
+            int halfDiff = (mThisWidth - width) / 2;
+            mCropRect.set(halfDiff, 0, width + halfDiff, mThisHeight);
+        } else {
+            int halfDiff = (mThisHeight - height) / 2;
+            mCropRect.set(0, halfDiff, mThisWidth, height + halfDiff);
+        }
+
+        calculateImageScaleBounds(drawableWidth, drawableHeight);
+        setupInitialImagePosition(drawableWidth, drawableHeight);
+
+        if (mCropBoundsChangeListener != null) {
+            mCropBoundsChangeListener.onCropAspectRatioChanged(mTargetAspectRatio);
+        }
+        if (mTransformImageListener != null) {
+            mTransformImageListener.onScale(getCurrentScale());
+            mTransformImageListener.onRotate(getCurrentAngle());
+        }
+    }
+
+    /**
+     * This method checks whether current image fills the crop bounds.
+     */
+    protected boolean isImageWrapCropBounds() {
+        return isImageWrapCropBounds(mCurrentImageCorners);
+    }
+
+    /**
+     * This methods checks whether a rectangle that is represented as 4 corner points (8 floats)
+     * fills the crop bounds rectangle.
+     *
+     * @param imageCorners - corners of a rectangle
+     * @return - true if it wraps crop bounds, false - otherwise
+     */
+    protected boolean isImageWrapCropBounds(float[] imageCorners) {
+        mTempMatrix.reset();
+        mTempMatrix.setRotate(-getCurrentAngle());
+
+        float[] unrotatedImageCorners = Arrays.copyOf(imageCorners, imageCorners.length);
+        mTempMatrix.mapPoints(unrotatedImageCorners);
+
+        float[] unrotatedCropBoundsCorners = RectUtils.getCornersFromRect(mCropRect);
+        mTempMatrix.mapPoints(unrotatedCropBoundsCorners);
+
+        return RectUtils.trapToRect(unrotatedImageCorners).contains(RectUtils.trapToRect(unrotatedCropBoundsCorners));
+    }
+
+    /**
+     * This method changes image scale (animating zoom for given duration), related to given center (x,y).
+     *
+     * @param scale      - target scale
+     * @param centerX    - scale center X
+     * @param centerY    - scale center Y
+     * @param durationMs - zoom animation duration
+     */
+    protected void zoomImageToPosition(float scale, float centerX, float centerY, long durationMs) {
+        if (scale > getMaxScale()) {
+            scale = getMaxScale();
+        }
+
+        final float oldScale = getCurrentScale();
+        final float deltaScale = scale - oldScale;
+
+        post(mZoomImageToPositionRunnable = new ZoomImageToPosition(CropImageView.this,
+                durationMs, oldScale, deltaScale, centerX, centerY));
+    }
+
+    private void calculateImageScaleBounds() {
+        final Drawable drawable = getDrawable();
+        if (drawable == null) {
+            return;
+        }
+        calculateImageScaleBounds(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
+    }
+
+    /**
+     * This method calculates image minimum and maximum scale values for current {@link #mCropRect}.
+     *
+     * @param drawableWidth  - image width
+     * @param drawableHeight - image height
+     */
+    private void calculateImageScaleBounds(float drawableWidth, float drawableHeight) {
+        float widthScale = Math.min(mCropRect.width() / drawableWidth, mCropRect.width() / drawableHeight);
+        float heightScale = Math.min(mCropRect.height() / drawableHeight, mCropRect.height() / drawableWidth);
+
+        mMinScale = Math.min(widthScale, heightScale);
+        mMaxScale = mMinScale * mMaxScaleMultiplier;
+    }
+
+    /**
+     * This method calculates initial image position so it is positioned properly.
+     * Then it sets those values to the current image matrix.
+     *
+     * @param drawableWidth  - image width
+     * @param drawableHeight - image height
+     */
+    private void setupInitialImagePosition(float drawableWidth, float drawableHeight) {
+        float cropRectWidth = mCropRect.width();
+        float cropRectHeight = mCropRect.height();
+
+        float widthScale = mCropRect.width() / drawableWidth;
+        float heightScale = mCropRect.height() / drawableHeight;
+
+        float initialMinScale = Math.max(widthScale, heightScale);
+
+        float tw = (cropRectWidth - drawableWidth * initialMinScale) / 2.0f + mCropRect.left;
+        float th = (cropRectHeight - drawableHeight * initialMinScale) / 2.0f + mCropRect.top;
+
+        mCurrentImageMatrix.reset();
+        mCurrentImageMatrix.postScale(initialMinScale, initialMinScale);
+        mCurrentImageMatrix.postTranslate(tw, th);
+        setImageMatrix(mCurrentImageMatrix);
+    }
+
+    /**
+     * This method extracts all needed values from the styled attributes.
+     * Those are used to configure the view.
+     */
+    @SuppressWarnings("deprecation")
+    protected void processStyledAttributes(@NonNull TypedArray a) {
+        float targetAspectRatioX = Math.abs(a.getFloat(R.styleable.ucrop_UCropView_ucrop_aspect_ratio_x, DEFAULT_ASPECT_RATIO));
+        float targetAspectRatioY = Math.abs(a.getFloat(R.styleable.ucrop_UCropView_ucrop_aspect_ratio_y, DEFAULT_ASPECT_RATIO));
+
+        if (targetAspectRatioX == SOURCE_IMAGE_ASPECT_RATIO || targetAspectRatioY == SOURCE_IMAGE_ASPECT_RATIO) {
+            mTargetAspectRatio = SOURCE_IMAGE_ASPECT_RATIO;
+        } else {
+            mTargetAspectRatio = targetAspectRatioX / targetAspectRatioY;
+        }
+    }
+
+    /**
+     * This Runnable is used to animate an image so it fills the crop bounds entirely.
+     * Given values are interpolated during the animation time.
+     * Runnable can be terminated either vie {@link #cancelAllAnimations()} method
+     * or when certain conditions inside {@link WrapCropBoundsRunnable#run()} method are triggered.
+     */
+    private static class WrapCropBoundsRunnable implements Runnable {
+
+        private final WeakReference<CropImageView> mCropImageView;
+
+        private final long mDurationMs, mStartTime;
+        private final float mOldX, mOldY;
+        private final float mCenterDiffX, mCenterDiffY;
+        private final float mOldScale;
+        private final float mDeltaScale;
+        private final boolean mWillBeImageInBoundsAfterTranslate;
+
+        public WrapCropBoundsRunnable(CropImageView cropImageView,
+                                      long durationMs,
+                                      float oldX, float oldY,
+                                      float centerDiffX, float centerDiffY,
+                                      float oldScale, float deltaScale,
+                                      boolean willBeImageInBoundsAfterTranslate) {
+
+            mCropImageView = new WeakReference<>(cropImageView);
+
+            mDurationMs = durationMs;
+            mStartTime = System.currentTimeMillis();
+            mOldX = oldX;
+            mOldY = oldY;
+            mCenterDiffX = centerDiffX;
+            mCenterDiffY = centerDiffY;
+            mOldScale = oldScale;
+            mDeltaScale = deltaScale;
+            mWillBeImageInBoundsAfterTranslate = willBeImageInBoundsAfterTranslate;
+        }
+
+        @Override
+        public void run() {
+            CropImageView cropImageView = mCropImageView.get();
+            if (cropImageView == null) {
+                return;
+            }
+
+            long now = System.currentTimeMillis();
+            float currentMs = Math.min(mDurationMs, now - mStartTime);
+
+            float newX = CubicEasing.easeOut(currentMs, 0, mCenterDiffX, mDurationMs);
+            float newY = CubicEasing.easeOut(currentMs, 0, mCenterDiffY, mDurationMs);
+            float newScale = CubicEasing.easeInOut(currentMs, 0, mDeltaScale, mDurationMs);
+
+            if (currentMs < mDurationMs) {
+                cropImageView.postTranslate(newX - (cropImageView.mCurrentImageCenter[0] - mOldX), newY - (cropImageView.mCurrentImageCenter[1] - mOldY));
+                if (!mWillBeImageInBoundsAfterTranslate) {
+                    cropImageView.zoomInImage(mOldScale + newScale, cropImageView.mCropRect.centerX(), cropImageView.mCropRect.centerY());
+                }
+                if (!cropImageView.isImageWrapCropBounds()) {
+                    cropImageView.post(this);
+                }
+            }
+        }
+    }
+
+    /**
+     * This Runnable is used to animate an image zoom.
+     * Given values are interpolated during the animation time.
+     * Runnable can be terminated either vie {@link #cancelAllAnimations()} method
+     * or when certain conditions inside {@link ZoomImageToPosition#run()} method are triggered.
+     */
+    private static class ZoomImageToPosition implements Runnable {
+
+        private final WeakReference<CropImageView> mCropImageView;
+
+        private final long mDurationMs, mStartTime;
+        private final float mOldScale;
+        private final float mDeltaScale;
+        private final float mDestX;
+        private final float mDestY;
+
+        public ZoomImageToPosition(CropImageView cropImageView,
+                                   long durationMs,
+                                   float oldScale, float deltaScale,
+                                   float destX, float destY) {
+
+            mCropImageView = new WeakReference<>(cropImageView);
+
+            mStartTime = System.currentTimeMillis();
+            mDurationMs = durationMs;
+            mOldScale = oldScale;
+            mDeltaScale = deltaScale;
+            mDestX = destX;
+            mDestY = destY;
+        }
+
+        @Override
+        public void run() {
+            CropImageView cropImageView = mCropImageView.get();
+            if (cropImageView == null) {
+                return;
+            }
+
+            long now = System.currentTimeMillis();
+            float currentMs = Math.min(mDurationMs, now - mStartTime);
+            float newScale = CubicEasing.easeInOut(currentMs, 0, mDeltaScale, mDurationMs);
+
+            if (currentMs < mDurationMs) {
+                cropImageView.zoomInImage(mOldScale + newScale, mDestX, mDestY);
+                cropImageView.post(this);
+            } else {
+                cropImageView.setImageToWrapCropBounds();
+            }
+        }
+
+    }
+
+}

+ 152 - 0
ucrop/src/main/java/com/yalantis/ucrop/view/GestureCropImageView.java

@@ -0,0 +1,152 @@
+package com.yalantis.ucrop.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+
+import com.yalantis.ucrop.util.RotationGestureDetector;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ */
+public class GestureCropImageView extends CropImageView {
+
+    private static final int DOUBLE_TAP_ZOOM_DURATION = 200;
+
+    private ScaleGestureDetector mScaleDetector;
+    private RotationGestureDetector mRotateDetector;
+    private GestureDetector mGestureDetector;
+
+    private float mMidPntX, mMidPntY;
+
+    private boolean mIsRotateEnabled = true, mIsScaleEnabled = true;
+    private int mDoubleTapScaleSteps = 5;
+
+    public GestureCropImageView(Context context) {
+        super(context);
+    }
+
+    public GestureCropImageView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public GestureCropImageView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public void setScaleEnabled(boolean scaleEnabled) {
+        mIsScaleEnabled = scaleEnabled;
+    }
+
+    public boolean isScaleEnabled() {
+        return mIsScaleEnabled;
+    }
+
+    public void setRotateEnabled(boolean rotateEnabled) {
+        mIsRotateEnabled = rotateEnabled;
+    }
+
+    public boolean isRotateEnabled() {
+        return mIsRotateEnabled;
+    }
+
+    public void setDoubleTapScaleSteps(int doubleTapScaleSteps) {
+        mDoubleTapScaleSteps = doubleTapScaleSteps;
+    }
+
+    public int getDoubleTapScaleSteps() {
+        return mDoubleTapScaleSteps;
+    }
+
+    /**
+     * If it's ACTION_DOWN event - user touches the screen and all current animation must be canceled.
+     * If it's ACTION_UP event - user removed all fingers from the screen and current image position must be corrected.
+     * If there are more than 2 fingers - update focal point coordinates.
+     * Pass the event to the gesture detectors if those are enabled.
+     */
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
+            cancelAllAnimations();
+        }
+
+        if (event.getPointerCount() > 1) {
+            mMidPntX = (event.getX(0) + event.getX(1)) / 2;
+            mMidPntY = (event.getY(0) + event.getY(1)) / 2;
+        }
+
+        mGestureDetector.onTouchEvent(event);
+
+        if (mIsScaleEnabled) {
+            mScaleDetector.onTouchEvent(event);
+        }
+
+        if (mIsRotateEnabled) {
+            mRotateDetector.onTouchEvent(event);
+        }
+
+        if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
+            setImageToWrapCropBounds();
+        }
+        return true;
+    }
+
+    @Override
+    protected void init() {
+        super.init();
+        setupGestureListeners();
+    }
+
+    /**
+     * This method calculates target scale value for double tap gesture.
+     * User is able to zoom the image from min scale value
+     * to the max scale value with {@link #mDoubleTapScaleSteps} double taps.
+     */
+    protected float getDoubleTapTargetScale() {
+        return getCurrentScale() * (float) Math.pow(getMaxScale() / getMinScale(), 1.0f / mDoubleTapScaleSteps);
+    }
+
+    private void setupGestureListeners() {
+        mGestureDetector = new GestureDetector(getContext(), new GestureListener(), null, true);
+        mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
+        mRotateDetector = new RotationGestureDetector(new RotateListener());
+    }
+
+    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+
+        @Override
+        public boolean onScale(ScaleGestureDetector detector) {
+            postScale(detector.getScaleFactor(), mMidPntX, mMidPntY);
+            return true;
+        }
+    }
+
+    private class GestureListener extends GestureDetector.SimpleOnGestureListener {
+
+        @Override
+        public boolean onDoubleTap(MotionEvent e) {
+            zoomImageToPosition(getDoubleTapTargetScale(), e.getX(), e.getY(), DOUBLE_TAP_ZOOM_DURATION);
+            return super.onDoubleTap(e);
+        }
+
+        @Override
+        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+            postTranslate(-distanceX, -distanceY);
+            return true;
+        }
+
+    }
+
+    private class RotateListener extends RotationGestureDetector.SimpleOnRotationGestureListener {
+
+        @Override
+        public boolean onRotation(RotationGestureDetector rotationDetector) {
+            postRotate(rotationDetector.getAngle(), mMidPntX, mMidPntY);
+            return true;
+        }
+
+    }
+
+}

+ 577 - 0
ucrop/src/main/java/com/yalantis/ucrop/view/OverlayView.java

@@ -0,0 +1,577 @@
+package com.yalantis.ucrop.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.os.Build;
+import android.support.annotation.ColorInt;
+import android.support.annotation.IntDef;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.yalantis.ucrop.R;
+import com.yalantis.ucrop.callback.OverlayViewChangeListener;
+import com.yalantis.ucrop.util.RectUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ * <p/>
+ * This view is used for drawing the overlay on top of the image. It may have frame, crop guidelines and dimmed area.
+ * This must have LAYER_TYPE_SOFTWARE to draw itself properly.
+ */
+public class OverlayView extends View {
+
+    public static final int FREESTYLE_CROP_MODE_DISABLE = 0;
+    public static final int FREESTYLE_CROP_MODE_ENABLE = 1;
+    public static final int FREESTYLE_CROP_MODE_ENABLE_WITH_PASS_THROUGH = 2;
+
+    public static final boolean DEFAULT_SHOW_CROP_FRAME = true;
+    public static final boolean DEFAULT_SHOW_CROP_GRID = true;
+    public static final boolean DEFAULT_CIRCLE_DIMMED_LAYER = false;
+    public static final int DEFAULT_FREESTYLE_CROP_MODE = FREESTYLE_CROP_MODE_DISABLE;
+    public static final int DEFAULT_CROP_GRID_ROW_COUNT = 2;
+    public static final int DEFAULT_CROP_GRID_COLUMN_COUNT = 2;
+
+    private final RectF mCropViewRect = new RectF();
+    private final RectF mTempRect = new RectF();
+
+    protected int mThisWidth, mThisHeight;
+    protected float[] mCropGridCorners;
+    protected float[] mCropGridCenter;
+    
+    private int mCropGridRowCount, mCropGridColumnCount;
+    private float mTargetAspectRatio;
+    private float[] mGridPoints = null;
+    private boolean mShowCropFrame, mShowCropGrid;
+    private boolean mCircleDimmedLayer;
+    private int mDimmedColor;
+    private Path mCircularPath = new Path();
+    private Paint mDimmedStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private Paint mCropGridPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private Paint mCropFramePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private Paint mCropFrameCornersPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    @FreestyleMode
+    private int mFreestyleCropMode = DEFAULT_FREESTYLE_CROP_MODE;
+    private float mPreviousTouchX = -1, mPreviousTouchY = -1;
+    private int mCurrentTouchCornerIndex = -1;
+    private int mTouchPointThreshold;
+    private int mCropRectMinSize;
+    private int mCropRectCornerTouchAreaLineLength;
+
+    private OverlayViewChangeListener mCallback;
+
+    private boolean mShouldSetupCropBounds;
+
+    {
+        mTouchPointThreshold = getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_rect_corner_touch_threshold);
+        mCropRectMinSize = getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_rect_min_size);
+        mCropRectCornerTouchAreaLineLength = getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_rect_corner_touch_area_line_length);
+    }
+
+    public OverlayView(Context context) {
+        this(context, null);
+    }
+
+    public OverlayView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public OverlayView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init();
+    }
+
+    public OverlayViewChangeListener getOverlayViewChangeListener() {
+        return mCallback;
+    }
+
+    public void setOverlayViewChangeListener(OverlayViewChangeListener callback) {
+        mCallback = callback;
+    }
+
+    @NonNull
+    public RectF getCropViewRect() {
+        return mCropViewRect;
+    }
+
+    @Deprecated
+    /***
+     * Please use the new method {@link #getFreestyleCropMode() getFreestyleCropMode} method as we have more than 1 freestyle crop mode.
+     */
+    public boolean isFreestyleCropEnabled() {
+        return mFreestyleCropMode == FREESTYLE_CROP_MODE_ENABLE;
+    }
+
+    @Deprecated
+    /***
+     * Please use the new method {@link #setFreestyleCropMode setFreestyleCropMode} method as we have more than 1 freestyle crop mode.
+     */
+    public void setFreestyleCropEnabled(boolean freestyleCropEnabled) {
+        mFreestyleCropMode = freestyleCropEnabled ? FREESTYLE_CROP_MODE_ENABLE : FREESTYLE_CROP_MODE_DISABLE;
+    }
+
+    @FreestyleMode
+    public int getFreestyleCropMode() {
+        return mFreestyleCropMode;
+    }
+
+    public void setFreestyleCropMode(@FreestyleMode int mFreestyleCropMode) {
+        this.mFreestyleCropMode = mFreestyleCropMode;
+        postInvalidate();
+    }
+
+    /**
+     * Setter for {@link #mCircleDimmedLayer} variable.
+     *
+     * @param circleDimmedLayer - set it to true if you want dimmed layer to be an circle
+     */
+    public void setCircleDimmedLayer(boolean circleDimmedLayer) {
+        mCircleDimmedLayer = circleDimmedLayer;
+    }
+
+    /**
+     * Setter for crop grid rows count.
+     * Resets {@link #mGridPoints} variable because it is not valid anymore.
+     */
+    public void setCropGridRowCount(@IntRange(from = 0) int cropGridRowCount) {
+        mCropGridRowCount = cropGridRowCount;
+        mGridPoints = null;
+    }
+
+    /**
+     * Setter for crop grid columns count.
+     * Resets {@link #mGridPoints} variable because it is not valid anymore.
+     */
+    public void setCropGridColumnCount(@IntRange(from = 0) int cropGridColumnCount) {
+        mCropGridColumnCount = cropGridColumnCount;
+        mGridPoints = null;
+    }
+
+    /**
+     * Setter for {@link #mShowCropFrame} variable.
+     *
+     * @param showCropFrame - set to true if you want to see a crop frame rectangle on top of an image
+     */
+    public void setShowCropFrame(boolean showCropFrame) {
+        mShowCropFrame = showCropFrame;
+    }
+
+    /**
+     * Setter for {@link #mShowCropGrid} variable.
+     *
+     * @param showCropGrid - set to true if you want to see a crop grid on top of an image
+     */
+    public void setShowCropGrid(boolean showCropGrid) {
+        mShowCropGrid = showCropGrid;
+    }
+
+    /**
+     * Setter for {@link #mDimmedColor} variable.
+     *
+     * @param dimmedColor - desired color of dimmed area around the crop bounds
+     */
+    public void setDimmedColor(@ColorInt int dimmedColor) {
+        mDimmedColor = dimmedColor;
+    }
+
+    /**
+     * Setter for crop frame stroke width
+     */
+    public void setCropFrameStrokeWidth(@IntRange(from = 0) int width) {
+        mCropFramePaint.setStrokeWidth(width);
+    }
+
+    /**
+     * Setter for crop grid stroke width
+     */
+    public void setCropGridStrokeWidth(@IntRange(from = 0) int width) {
+        mCropGridPaint.setStrokeWidth(width);
+    }
+
+    /**
+     * Setter for crop frame color
+     */
+    public void setCropFrameColor(@ColorInt int color) {
+        mCropFramePaint.setColor(color);
+    }
+
+    /**
+     * Setter for crop grid color
+     */
+    public void setCropGridColor(@ColorInt int color) {
+        mCropGridPaint.setColor(color);
+    }
+
+    /**
+     * This method sets aspect ratio for crop bounds.
+     *
+     * @param targetAspectRatio - aspect ratio for image crop (e.g. 1.77(7) for 16:9)
+     */
+    public void setTargetAspectRatio(final float targetAspectRatio) {
+        mTargetAspectRatio = targetAspectRatio;
+        if (mThisWidth > 0) {
+            setupCropBounds();
+            postInvalidate();
+        } else {
+            mShouldSetupCropBounds = true;
+        }
+    }
+
+    /**
+     * This method setups crop bounds rectangles for given aspect ratio and view size.
+     * {@link #mCropViewRect} is used to draw crop bounds - uses padding.
+     */
+    public void setupCropBounds() {
+        int height = (int) (mThisWidth / mTargetAspectRatio);
+        if (height > mThisHeight) {
+            int width = (int) (mThisHeight * mTargetAspectRatio);
+            int halfDiff = (mThisWidth - width) / 2;
+            mCropViewRect.set(getPaddingLeft() + halfDiff, getPaddingTop(),
+                    getPaddingLeft() + width + halfDiff, getPaddingTop() + mThisHeight);
+        } else {
+            int halfDiff = (mThisHeight - height) / 2;
+            mCropViewRect.set(getPaddingLeft(), getPaddingTop() + halfDiff,
+                    getPaddingLeft() + mThisWidth, getPaddingTop() + height + halfDiff);
+        }
+
+        if (mCallback != null) {
+            mCallback.onCropRectUpdated(mCropViewRect);
+        }
+
+        updateGridPoints();
+    }
+
+    private void updateGridPoints() {
+        mCropGridCorners = RectUtils.getCornersFromRect(mCropViewRect);
+        mCropGridCenter = RectUtils.getCenterFromRect(mCropViewRect);
+
+        mGridPoints = null;
+        mCircularPath.reset();
+        mCircularPath.addCircle(mCropViewRect.centerX(), mCropViewRect.centerY(),
+                Math.min(mCropViewRect.width(), mCropViewRect.height()) / 2.f, Path.Direction.CW);
+    }
+
+    protected void init() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 &&
+                Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+            setLayerType(LAYER_TYPE_SOFTWARE, null);
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        if (changed) {
+            left = getPaddingLeft();
+            top = getPaddingTop();
+            right = getWidth() - getPaddingRight();
+            bottom = getHeight() - getPaddingBottom();
+            mThisWidth = right - left;
+            mThisHeight = bottom - top;
+
+            if (mShouldSetupCropBounds) {
+                mShouldSetupCropBounds = false;
+                setTargetAspectRatio(mTargetAspectRatio);
+            }
+        }
+    }
+
+    /**
+     * Along with image there are dimmed layer, crop bounds and crop guidelines that must be drawn.
+     */
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        drawDimmedLayer(canvas);
+        drawCropGrid(canvas);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (mCropViewRect.isEmpty() || mFreestyleCropMode == FREESTYLE_CROP_MODE_DISABLE) { return false; }
+
+        float x = event.getX();
+        float y = event.getY();
+
+        if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
+            mCurrentTouchCornerIndex = getCurrentTouchIndex(x, y);
+            boolean shouldHandle = mCurrentTouchCornerIndex != -1;
+            if (!shouldHandle) {
+                mPreviousTouchX = -1;
+                mPreviousTouchY = -1;
+            } else if (mPreviousTouchX < 0) {
+                mPreviousTouchX = x;
+                mPreviousTouchY = y;
+            }
+            return shouldHandle;
+        }
+
+        if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_MOVE) {
+            if (event.getPointerCount() == 1 && mCurrentTouchCornerIndex != -1) {
+
+                x = Math.min(Math.max(x, getPaddingLeft()), getWidth() - getPaddingRight());
+                y = Math.min(Math.max(y, getPaddingTop()), getHeight() - getPaddingBottom());
+
+                updateCropViewRect(x, y);
+
+                mPreviousTouchX = x;
+                mPreviousTouchY = y;
+
+                return true;
+            }
+        }
+
+        if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
+            mPreviousTouchX = -1;
+            mPreviousTouchY = -1;
+            mCurrentTouchCornerIndex = -1;
+
+            if (mCallback != null) {
+                mCallback.onCropRectUpdated(mCropViewRect);
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * * The order of the corners is:
+     * 0------->1
+     * ^        |
+     * |   4    |
+     * |        v
+     * 3<-------2
+     */
+    private void updateCropViewRect(float touchX, float touchY) {
+        mTempRect.set(mCropViewRect);
+
+        switch (mCurrentTouchCornerIndex) {
+            // resize rectangle
+            case 0:
+                mTempRect.set(touchX, touchY, mCropViewRect.right, mCropViewRect.bottom);
+                break;
+            case 1:
+                mTempRect.set(mCropViewRect.left, touchY, touchX, mCropViewRect.bottom);
+                break;
+            case 2:
+                mTempRect.set(mCropViewRect.left, mCropViewRect.top, touchX, touchY);
+                break;
+            case 3:
+                mTempRect.set(touchX, mCropViewRect.top, mCropViewRect.right, touchY);
+                break;
+            // move rectangle
+            case 4:
+                mTempRect.offset(touchX - mPreviousTouchX, touchY - mPreviousTouchY);
+                if (mTempRect.left > getLeft() && mTempRect.top > getTop()
+                        && mTempRect.right < getRight() && mTempRect.bottom < getBottom()) {
+                    mCropViewRect.set(mTempRect);
+                    updateGridPoints();
+                    postInvalidate();
+                }
+                return;
+        }
+
+        boolean changeHeight = mTempRect.height() >= mCropRectMinSize;
+        boolean changeWidth = mTempRect.width() >= mCropRectMinSize;
+        mCropViewRect.set(
+                changeWidth ? mTempRect.left : mCropViewRect.left,
+                changeHeight ? mTempRect.top : mCropViewRect.top,
+                changeWidth ? mTempRect.right : mCropViewRect.right,
+                changeHeight ? mTempRect.bottom : mCropViewRect.bottom);
+
+        if (changeHeight || changeWidth) {
+            updateGridPoints();
+            postInvalidate();
+        }
+    }
+
+    /**
+     * * The order of the corners in the float array is:
+     * 0------->1
+     * ^        |
+     * |   4    |
+     * |        v
+     * 3<-------2
+     *
+     * @return - index of corner that is being dragged
+     */
+    private int getCurrentTouchIndex(float touchX, float touchY) {
+        int closestPointIndex = -1;
+        double closestPointDistance = mTouchPointThreshold;
+        for (int i = 0; i < 8; i += 2) {
+            double distanceToCorner = Math.sqrt(Math.pow(touchX - mCropGridCorners[i], 2)
+                    + Math.pow(touchY - mCropGridCorners[i + 1], 2));
+            if (distanceToCorner < closestPointDistance) {
+                closestPointDistance = distanceToCorner;
+                closestPointIndex = i / 2;
+            }
+        }
+
+        if (mFreestyleCropMode == FREESTYLE_CROP_MODE_ENABLE && closestPointIndex < 0 && mCropViewRect.contains(touchX, touchY)) {
+            return 4;
+        }
+
+//        for (int i = 0; i <= 8; i += 2) {
+//
+//            double distanceToCorner;
+//            if (i < 8) { // corners
+//                distanceToCorner = Math.sqrt(Math.pow(touchX - mCropGridCorners[i], 2)
+//                        + Math.pow(touchY - mCropGridCorners[i + 1], 2));
+//            } else { // center
+//                distanceToCorner = Math.sqrt(Math.pow(touchX - mCropGridCenter[0], 2)
+//                        + Math.pow(touchY - mCropGridCenter[1], 2));
+//            }
+//            if (distanceToCorner < closestPointDistance) {
+//                closestPointDistance = distanceToCorner;
+//                closestPointIndex = i / 2;
+//            }
+//        }
+        return closestPointIndex;
+    }
+
+    /**
+     * This method draws dimmed area around the crop bounds.
+     *
+     * @param canvas - valid canvas object
+     */
+    protected void drawDimmedLayer(@NonNull Canvas canvas) {
+        canvas.save();
+        if (mCircleDimmedLayer) {
+            canvas.clipPath(mCircularPath, Region.Op.DIFFERENCE);
+        } else {
+            canvas.clipRect(mCropViewRect, Region.Op.DIFFERENCE);
+        }
+        canvas.drawColor(mDimmedColor);
+        canvas.restore();
+
+        if (mCircleDimmedLayer) { // Draw 1px stroke to fix antialias
+            canvas.drawCircle(mCropViewRect.centerX(), mCropViewRect.centerY(),
+                    Math.min(mCropViewRect.width(), mCropViewRect.height()) / 2.f, mDimmedStrokePaint);
+        }
+    }
+
+    /**
+     * This method draws crop bounds (empty rectangle)
+     * and crop guidelines (vertical and horizontal lines inside the crop bounds) if needed.
+     *
+     * @param canvas - valid canvas object
+     */
+    protected void drawCropGrid(@NonNull Canvas canvas) {
+        if (mShowCropGrid) {
+            if (mGridPoints == null && !mCropViewRect.isEmpty()) {
+
+                mGridPoints = new float[(mCropGridRowCount) * 4 + (mCropGridColumnCount) * 4];
+
+                int index = 0;
+                for (int i = 0; i < mCropGridRowCount; i++) {
+                    mGridPoints[index++] = mCropViewRect.left;
+                    mGridPoints[index++] = (mCropViewRect.height() * (((float) i + 1.0f) / (float) (mCropGridRowCount + 1))) + mCropViewRect.top;
+                    mGridPoints[index++] = mCropViewRect.right;
+                    mGridPoints[index++] = (mCropViewRect.height() * (((float) i + 1.0f) / (float) (mCropGridRowCount + 1))) + mCropViewRect.top;
+                }
+
+                for (int i = 0; i < mCropGridColumnCount; i++) {
+                    mGridPoints[index++] = (mCropViewRect.width() * (((float) i + 1.0f) / (float) (mCropGridColumnCount + 1))) + mCropViewRect.left;
+                    mGridPoints[index++] = mCropViewRect.top;
+                    mGridPoints[index++] = (mCropViewRect.width() * (((float) i + 1.0f) / (float) (mCropGridColumnCount + 1))) + mCropViewRect.left;
+                    mGridPoints[index++] = mCropViewRect.bottom;
+                }
+            }
+
+            if (mGridPoints != null) {
+                canvas.drawLines(mGridPoints, mCropGridPaint);
+            }
+        }
+
+        if (mShowCropFrame) {
+            canvas.drawRect(mCropViewRect, mCropFramePaint);
+        }
+
+        if (mFreestyleCropMode != FREESTYLE_CROP_MODE_DISABLE) {
+            canvas.save();
+
+            mTempRect.set(mCropViewRect);
+            mTempRect.inset(mCropRectCornerTouchAreaLineLength, -mCropRectCornerTouchAreaLineLength);
+            canvas.clipRect(mTempRect, Region.Op.DIFFERENCE);
+
+            mTempRect.set(mCropViewRect);
+            mTempRect.inset(-mCropRectCornerTouchAreaLineLength, mCropRectCornerTouchAreaLineLength);
+            canvas.clipRect(mTempRect, Region.Op.DIFFERENCE);
+
+            canvas.drawRect(mCropViewRect, mCropFrameCornersPaint);
+
+            canvas.restore();
+        }
+    }
+
+    /**
+     * This method extracts all needed values from the styled attributes.
+     * Those are used to configure the view.
+     */
+    @SuppressWarnings("deprecation")
+    protected void processStyledAttributes(@NonNull TypedArray a) {
+        mCircleDimmedLayer = a.getBoolean(R.styleable.ucrop_UCropView_ucrop_circle_dimmed_layer, DEFAULT_CIRCLE_DIMMED_LAYER);
+        mDimmedColor = a.getColor(R.styleable.ucrop_UCropView_ucrop_dimmed_color,
+                getResources().getColor(R.color.ucrop_color_default_dimmed));
+        mDimmedStrokePaint.setColor(mDimmedColor);
+        mDimmedStrokePaint.setStyle(Paint.Style.STROKE);
+        mDimmedStrokePaint.setStrokeWidth(1);
+
+        initCropFrameStyle(a);
+        mShowCropFrame = a.getBoolean(R.styleable.ucrop_UCropView_ucrop_show_frame, DEFAULT_SHOW_CROP_FRAME);
+
+        initCropGridStyle(a);
+        mShowCropGrid = a.getBoolean(R.styleable.ucrop_UCropView_ucrop_show_grid, DEFAULT_SHOW_CROP_GRID);
+    }
+
+    /**
+     * This method setups Paint object for the crop bounds.
+     */
+    @SuppressWarnings("deprecation")
+    private void initCropFrameStyle(@NonNull TypedArray a) {
+        int cropFrameStrokeSize = a.getDimensionPixelSize(R.styleable.ucrop_UCropView_ucrop_frame_stroke_size,
+                getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_frame_stoke_width));
+        int cropFrameColor = a.getColor(R.styleable.ucrop_UCropView_ucrop_frame_color,
+                getResources().getColor(R.color.ucrop_color_default_crop_frame));
+        mCropFramePaint.setStrokeWidth(cropFrameStrokeSize);
+        mCropFramePaint.setColor(cropFrameColor);
+        mCropFramePaint.setStyle(Paint.Style.STROKE);
+
+        mCropFrameCornersPaint.setStrokeWidth(cropFrameStrokeSize * 3);
+        mCropFrameCornersPaint.setColor(cropFrameColor);
+        mCropFrameCornersPaint.setStyle(Paint.Style.STROKE);
+    }
+
+    /**
+     * This method setups Paint object for the crop guidelines.
+     */
+    @SuppressWarnings("deprecation")
+    private void initCropGridStyle(@NonNull TypedArray a) {
+        int cropGridStrokeSize = a.getDimensionPixelSize(R.styleable.ucrop_UCropView_ucrop_grid_stroke_size,
+                getResources().getDimensionPixelSize(R.dimen.ucrop_default_crop_grid_stoke_width));
+        int cropGridColor = a.getColor(R.styleable.ucrop_UCropView_ucrop_grid_color,
+                getResources().getColor(R.color.ucrop_color_default_crop_grid));
+        mCropGridPaint.setStrokeWidth(cropGridStrokeSize);
+        mCropGridPaint.setColor(cropGridColor);
+
+        mCropGridRowCount = a.getInt(R.styleable.ucrop_UCropView_ucrop_grid_row_count, DEFAULT_CROP_GRID_ROW_COUNT);
+        mCropGridColumnCount = a.getInt(R.styleable.ucrop_UCropView_ucrop_grid_column_count, DEFAULT_CROP_GRID_COLUMN_COUNT);
+    }
+
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({FREESTYLE_CROP_MODE_DISABLE, FREESTYLE_CROP_MODE_ENABLE, FREESTYLE_CROP_MODE_ENABLE_WITH_PASS_THROUGH})
+    public @interface FreestyleMode {
+    }
+
+}

+ 339 - 0
ucrop/src/main/java/com/yalantis/ucrop/view/TransformImageView.java

@@ -0,0 +1,339 @@
+package com.yalantis.ucrop.view;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.widget.ImageView;
+
+import com.yalantis.ucrop.callback.BitmapLoadCallback;
+import com.yalantis.ucrop.model.ExifInfo;
+import com.yalantis.ucrop.util.BitmapLoadUtils;
+import com.yalantis.ucrop.util.FastBitmapDrawable;
+import com.yalantis.ucrop.util.RectUtils;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ * <p/>
+ * This class provides base logic to setup the image, transform it with matrix (move, scale, rotate),
+ * and methods to get current matrix state.
+ */
+public class TransformImageView extends ImageView {
+
+    private static final String TAG = "TransformImageView";
+
+    private static final int RECT_CORNER_POINTS_COORDS = 8;
+    private static final int RECT_CENTER_POINT_COORDS = 2;
+    private static final int MATRIX_VALUES_COUNT = 9;
+
+    protected final float[] mCurrentImageCorners = new float[RECT_CORNER_POINTS_COORDS];
+    protected final float[] mCurrentImageCenter = new float[RECT_CENTER_POINT_COORDS];
+
+    private final float[] mMatrixValues = new float[MATRIX_VALUES_COUNT];
+
+    protected Matrix mCurrentImageMatrix = new Matrix();
+    protected int mThisWidth, mThisHeight;
+
+    protected TransformImageListener mTransformImageListener;
+
+    private float[] mInitialImageCorners;
+    private float[] mInitialImageCenter;
+
+    protected boolean mBitmapDecoded = false;
+    protected boolean mBitmapLaidOut = false;
+
+    private int mMaxBitmapSize = 0;
+
+    private String mImageInputPath, mImageOutputPath;
+    private ExifInfo mExifInfo;
+
+    /**
+     * Interface for rotation and scale change notifying.
+     */
+    public interface TransformImageListener {
+
+        void onLoadComplete();
+
+        void onLoadFailure(@NonNull Exception e);
+
+        void onRotate(float currentAngle);
+
+        void onScale(float currentScale);
+
+    }
+
+    public TransformImageView(Context context) {
+        this(context, null);
+    }
+
+    public TransformImageView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public TransformImageView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init();
+    }
+
+    public void setTransformImageListener(TransformImageListener transformImageListener) {
+        mTransformImageListener = transformImageListener;
+    }
+
+    @Override
+    public void setScaleType(ScaleType scaleType) {
+        if (scaleType == ScaleType.MATRIX) {
+            super.setScaleType(scaleType);
+        } else {
+            Log.w(TAG, "Invalid ScaleType. Only ScaleType.MATRIX can be used");
+        }
+    }
+
+    /**
+     * Setter for {@link #mMaxBitmapSize} value.
+     * Be sure to call it before {@link #setImageURI(Uri)} or other image setters.
+     *
+     * @param maxBitmapSize - max size for both width and height of bitmap that will be used in the view.
+     */
+    public void setMaxBitmapSize(int maxBitmapSize) {
+        mMaxBitmapSize = maxBitmapSize;
+    }
+
+    public int getMaxBitmapSize() {
+        if (mMaxBitmapSize <= 0) {
+            mMaxBitmapSize = BitmapLoadUtils.calculateMaxBitmapSize(getContext());
+        }
+        return mMaxBitmapSize;
+    }
+
+    @Override
+    public void setImageBitmap(final Bitmap bitmap) {
+        setImageDrawable(new FastBitmapDrawable(bitmap));
+    }
+
+    public String getImageInputPath() {
+        return mImageInputPath;
+    }
+
+    public String getImageOutputPath() {
+        return mImageOutputPath;
+    }
+
+    public ExifInfo getExifInfo() {
+        return mExifInfo;
+    }
+
+    /**
+     * This method takes an Uri as a parameter, then calls method to decode it into Bitmap with specified size.
+     *
+     * @param imageUri - image Uri
+     * @throws Exception - can throw exception if having problems with decoding Uri or OOM.
+     */
+    public void setImageUri(@NonNull Uri imageUri, @Nullable Uri outputUri) throws Exception {
+        int maxBitmapSize = getMaxBitmapSize();
+
+        BitmapLoadUtils.decodeBitmapInBackground(getContext(), imageUri, outputUri, maxBitmapSize, maxBitmapSize,
+                new BitmapLoadCallback() {
+
+                    @Override
+                    public void onBitmapLoaded(@NonNull Bitmap bitmap, @NonNull ExifInfo exifInfo, @NonNull String imageInputPath, @Nullable String imageOutputPath) {
+                        mImageInputPath = imageInputPath;
+                        mImageOutputPath = imageOutputPath;
+                        mExifInfo = exifInfo;
+
+                        mBitmapDecoded = true;
+                        setImageBitmap(bitmap);
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Exception bitmapWorkerException) {
+                        Log.e(TAG, "onFailure: setImageUri", bitmapWorkerException);
+                        if (mTransformImageListener != null) {
+                            mTransformImageListener.onLoadFailure(bitmapWorkerException);
+                        }
+                    }
+                });
+    }
+
+    /**
+     * @return - current image scale value.
+     * [1.0f - for original image, 2.0f - for 200% scaled image, etc.]
+     */
+    public float getCurrentScale() {
+        return getMatrixScale(mCurrentImageMatrix);
+    }
+
+    /**
+     * This method calculates scale value for given Matrix object.
+     */
+    public float getMatrixScale(@NonNull Matrix matrix) {
+        return (float) Math.sqrt(Math.pow(getMatrixValue(matrix, Matrix.MSCALE_X), 2)
+                + Math.pow(getMatrixValue(matrix, Matrix.MSKEW_Y), 2));
+    }
+
+    /**
+     * @return - current image rotation angle.
+     */
+    public float getCurrentAngle() {
+        return getMatrixAngle(mCurrentImageMatrix);
+    }
+
+    /**
+     * This method calculates rotation angle for given Matrix object.
+     */
+    public float getMatrixAngle(@NonNull Matrix matrix) {
+        return (float) -(Math.atan2(getMatrixValue(matrix, Matrix.MSKEW_X),
+                getMatrixValue(matrix, Matrix.MSCALE_X)) * (180 / Math.PI));
+    }
+
+    @Override
+    public void setImageMatrix(Matrix matrix) {
+        super.setImageMatrix(matrix);
+        mCurrentImageMatrix.set(matrix);
+        updateCurrentImagePoints();
+    }
+
+    @Nullable
+    public Bitmap getViewBitmap() {
+        if (getDrawable() == null || !(getDrawable() instanceof FastBitmapDrawable)) {
+            return null;
+        } else {
+            return ((FastBitmapDrawable) getDrawable()).getBitmap();
+        }
+    }
+
+    /**
+     * This method translates current image.
+     *
+     * @param deltaX - horizontal shift
+     * @param deltaY - vertical shift
+     */
+    public void postTranslate(float deltaX, float deltaY) {
+        if (deltaX != 0 || deltaY != 0) {
+            mCurrentImageMatrix.postTranslate(deltaX, deltaY);
+            setImageMatrix(mCurrentImageMatrix);
+        }
+    }
+
+    /**
+     * This method scales current image.
+     *
+     * @param deltaScale - scale value
+     * @param px         - scale center X
+     * @param py         - scale center Y
+     */
+    public void postScale(float deltaScale, float px, float py) {
+        if (deltaScale != 0) {
+            mCurrentImageMatrix.postScale(deltaScale, deltaScale, px, py);
+            setImageMatrix(mCurrentImageMatrix);
+            if (mTransformImageListener != null) {
+                mTransformImageListener.onScale(getMatrixScale(mCurrentImageMatrix));
+            }
+        }
+    }
+
+    /**
+     * This method rotates current image.
+     *
+     * @param deltaAngle - rotation angle
+     * @param px         - rotation center X
+     * @param py         - rotation center Y
+     */
+    public void postRotate(float deltaAngle, float px, float py) {
+        if (deltaAngle != 0) {
+            mCurrentImageMatrix.postRotate(deltaAngle, px, py);
+            setImageMatrix(mCurrentImageMatrix);
+            if (mTransformImageListener != null) {
+                mTransformImageListener.onRotate(getMatrixAngle(mCurrentImageMatrix));
+            }
+        }
+    }
+
+    protected void init() {
+        setScaleType(ScaleType.MATRIX);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        if (changed || (mBitmapDecoded && !mBitmapLaidOut)) {
+
+            left = getPaddingLeft();
+            top = getPaddingTop();
+            right = getWidth() - getPaddingRight();
+            bottom = getHeight() - getPaddingBottom();
+            mThisWidth = right - left;
+            mThisHeight = bottom - top;
+
+            onImageLaidOut();
+        }
+    }
+
+    /**
+     * When image is laid out {@link #mInitialImageCenter} and {@link #mInitialImageCenter}
+     * must be set.
+     */
+    protected void onImageLaidOut() {
+        final Drawable drawable = getDrawable();
+        if (drawable == null) {
+            return;
+        }
+
+        float w = drawable.getIntrinsicWidth();
+        float h = drawable.getIntrinsicHeight();
+
+        Log.d(TAG, String.format("Image size: [%d:%d]", (int) w, (int) h));
+
+        RectF initialImageRect = new RectF(0, 0, w, h);
+        mInitialImageCorners = RectUtils.getCornersFromRect(initialImageRect);
+        mInitialImageCenter = RectUtils.getCenterFromRect(initialImageRect);
+
+        mBitmapLaidOut = true;
+
+        if (mTransformImageListener != null) {
+            mTransformImageListener.onLoadComplete();
+        }
+    }
+
+    /**
+     * This method returns Matrix value for given index.
+     *
+     * @param matrix     - valid Matrix object
+     * @param valueIndex - index of needed value. See {@link Matrix#MSCALE_X} and others.
+     * @return - matrix value for index
+     */
+    protected float getMatrixValue(@NonNull Matrix matrix, @IntRange(from = 0, to = MATRIX_VALUES_COUNT) int valueIndex) {
+        matrix.getValues(mMatrixValues);
+        return mMatrixValues[valueIndex];
+    }
+
+    /**
+     * This method logs given matrix X, Y, scale, and angle values.
+     * Can be used for debug.
+     */
+    @SuppressWarnings("unused")
+    protected void printMatrix(@NonNull String logPrefix, @NonNull Matrix matrix) {
+        float x = getMatrixValue(matrix, Matrix.MTRANS_X);
+        float y = getMatrixValue(matrix, Matrix.MTRANS_Y);
+        float rScale = getMatrixScale(matrix);
+        float rAngle = getMatrixAngle(matrix);
+        Log.d(TAG, logPrefix + ": matrix: { x: " + x + ", y: " + y + ", scale: " + rScale + ", angle: " + rAngle + " }");
+    }
+
+    /**
+     * This method updates current image corners and center points that are stored in
+     * {@link #mCurrentImageCorners} and {@link #mCurrentImageCenter} arrays.
+     * Those are used for several calculations.
+     */
+    private void updateCurrentImagePoints() {
+        mCurrentImageMatrix.mapPoints(mCurrentImageCorners, mInitialImageCorners);
+        mCurrentImageMatrix.mapPoints(mCurrentImageCenter, mInitialImageCenter);
+    }
+
+}

+ 81 - 0
ucrop/src/main/java/com/yalantis/ucrop/view/UCropView.java

@@ -0,0 +1,81 @@
+package com.yalantis.ucrop.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.RectF;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.widget.FrameLayout;
+
+import com.yalantis.ucrop.R;
+import com.yalantis.ucrop.callback.CropBoundsChangeListener;
+import com.yalantis.ucrop.callback.OverlayViewChangeListener;
+
+public class UCropView extends FrameLayout {
+
+    private GestureCropImageView mGestureCropImageView;
+    private final OverlayView mViewOverlay;
+
+    public UCropView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public UCropView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+
+        LayoutInflater.from(context).inflate(R.layout.ucrop_view, this, true);
+        mGestureCropImageView = (GestureCropImageView) findViewById(R.id.image_view_crop);
+        mViewOverlay = (OverlayView) findViewById(R.id.view_overlay);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ucrop_UCropView);
+        mViewOverlay.processStyledAttributes(a);
+        mGestureCropImageView.processStyledAttributes(a);
+        a.recycle();
+
+
+        setListenersToViews();
+    }
+
+    private void setListenersToViews() {
+        mGestureCropImageView.setCropBoundsChangeListener(new CropBoundsChangeListener() {
+            @Override
+            public void onCropAspectRatioChanged(float cropRatio) {
+                mViewOverlay.setTargetAspectRatio(cropRatio);
+            }
+        });
+        mViewOverlay.setOverlayViewChangeListener(new OverlayViewChangeListener() {
+            @Override
+            public void onCropRectUpdated(RectF cropRect) {
+                mGestureCropImageView.setCropRect(cropRect);
+            }
+        });
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return false;
+    }
+
+    @NonNull
+    public GestureCropImageView getCropImageView() {
+        return mGestureCropImageView;
+    }
+
+    @NonNull
+    public OverlayView getOverlayView() {
+        return mViewOverlay;
+    }
+
+    /**
+     * Method for reset state for UCropImageView such as rotation, scale, translation.
+     * Be careful: this method recreate UCropImageView instance and reattach it to layout.
+     */
+    public void resetCropImageView() {
+        removeView(mGestureCropImageView);
+        mGestureCropImageView = new GestureCropImageView(getContext());
+        setListenersToViews();
+        mGestureCropImageView.setCropRect(getOverlayView().getCropViewRect());
+        addView(mGestureCropImageView, 0);
+    }
+}

+ 163 - 0
ucrop/src/main/java/com/yalantis/ucrop/view/widget/AspectRatioTextView.java

@@ -0,0 +1,163 @@
+package com.yalantis.ucrop.view.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.os.Build;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.widget.TextView;
+
+import com.yalantis.ucrop.R;
+import com.yalantis.ucrop.model.AspectRatio;
+import com.yalantis.ucrop.view.CropImageView;
+
+import java.util.Locale;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ */
+public class AspectRatioTextView extends TextView {
+
+    private final Rect mCanvasClipBounds = new Rect();
+    private Paint mDotPaint;
+    private int mDotSize;
+    private float mAspectRatio;
+
+    private String mAspectRatioTitle;
+    private float mAspectRatioX, mAspectRatioY;
+
+    public AspectRatioTextView(Context context) {
+        this(context, null);
+    }
+
+    public AspectRatioTextView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AspectRatioTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ucrop_AspectRatioTextView);
+        init(a);
+    }
+
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    public AspectRatioTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ucrop_AspectRatioTextView);
+        init(a);
+    }
+
+    /**
+     * @param activeColor the resolved color for active elements
+     */
+
+    public void setActiveColor(@ColorInt int activeColor) {
+        applyActiveColor(activeColor);
+        invalidate();
+    }
+
+    public void setAspectRatio(@NonNull AspectRatio aspectRatio) {
+        mAspectRatioTitle = aspectRatio.getAspectRatioTitle();
+        mAspectRatioX = aspectRatio.getAspectRatioX();
+        mAspectRatioY = aspectRatio.getAspectRatioY();
+
+        if (mAspectRatioX == CropImageView.SOURCE_IMAGE_ASPECT_RATIO || mAspectRatioY == CropImageView.SOURCE_IMAGE_ASPECT_RATIO) {
+            mAspectRatio = CropImageView.SOURCE_IMAGE_ASPECT_RATIO;
+        } else {
+            mAspectRatio = mAspectRatioX / mAspectRatioY;
+        }
+
+        setTitle();
+    }
+
+    public float getAspectRatio(boolean toggleRatio) {
+        if (toggleRatio) {
+            toggleAspectRatio();
+            setTitle();
+        }
+        return mAspectRatio;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        if (isSelected()) {
+            canvas.getClipBounds(mCanvasClipBounds);
+            canvas.drawCircle((mCanvasClipBounds.right - mCanvasClipBounds.left) / 2.0f, mCanvasClipBounds.bottom - mDotSize,
+                    mDotSize / 2, mDotPaint);
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    private void init(@NonNull TypedArray a) {
+        setGravity(Gravity.CENTER_HORIZONTAL);
+
+        mAspectRatioTitle = a.getString(R.styleable.ucrop_AspectRatioTextView_ucrop_artv_ratio_title);
+        mAspectRatioX = a.getFloat(R.styleable.ucrop_AspectRatioTextView_ucrop_artv_ratio_x, CropImageView.SOURCE_IMAGE_ASPECT_RATIO);
+        mAspectRatioY = a.getFloat(R.styleable.ucrop_AspectRatioTextView_ucrop_artv_ratio_y, CropImageView.SOURCE_IMAGE_ASPECT_RATIO);
+
+        if (mAspectRatioX == CropImageView.SOURCE_IMAGE_ASPECT_RATIO || mAspectRatioY == CropImageView.SOURCE_IMAGE_ASPECT_RATIO) {
+            mAspectRatio = CropImageView.SOURCE_IMAGE_ASPECT_RATIO;
+        } else {
+            mAspectRatio = mAspectRatioX / mAspectRatioY;
+        }
+
+        mDotSize = getContext().getResources().getDimensionPixelSize(R.dimen.ucrop_size_dot_scale_text_view);
+        mDotPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mDotPaint.setStyle(Paint.Style.FILL);
+
+        setTitle();
+
+        int activeColor = getResources().getColor(R.color.ucrop_color_widget_active);
+        applyActiveColor(activeColor);
+
+        a.recycle();
+    }
+
+    private void applyActiveColor(@ColorInt int activeColor) {
+        if (mDotPaint != null) {
+            mDotPaint.setColor(activeColor);
+        }
+        ColorStateList textViewColorStateList = new ColorStateList(
+                new int[][]{
+                        new int[]{android.R.attr.state_selected},
+                        new int[]{0}
+                },
+                new int[]{
+                        activeColor,
+                        ContextCompat.getColor(getContext(), R.color.ucrop_color_widget)
+                }
+        );
+
+        setTextColor(textViewColorStateList);
+    }
+
+    private void toggleAspectRatio() {
+        if (mAspectRatio != CropImageView.SOURCE_IMAGE_ASPECT_RATIO) {
+            float tempRatioW = mAspectRatioX;
+            mAspectRatioX = mAspectRatioY;
+            mAspectRatioY = tempRatioW;
+
+            mAspectRatio = mAspectRatioX / mAspectRatioY;
+        }
+    }
+
+    private void setTitle() {
+        if (!TextUtils.isEmpty(mAspectRatioTitle)) {
+            setText(mAspectRatioTitle);
+        } else {
+            setText(String.format(Locale.US, "%d:%d", (int) mAspectRatioX, (int) mAspectRatioY));
+        }
+    }
+
+}

+ 151 - 0
ucrop/src/main/java/com/yalantis/ucrop/view/widget/HorizontalProgressWheelView.java

@@ -0,0 +1,151 @@
+package com.yalantis.ucrop.view.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.os.Build;
+import android.support.annotation.ColorInt;
+import android.support.v4.content.ContextCompat;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.yalantis.ucrop.R;
+
+/**
+ * Created by Oleksii Shliama (https://github.com/shliama).
+ */
+public class HorizontalProgressWheelView extends View {
+
+    private final Rect mCanvasClipBounds = new Rect();
+
+    private ScrollingListener mScrollingListener;
+    private float mLastTouchedPosition;
+
+    private Paint mProgressLinePaint;
+    private int mProgressLineWidth, mProgressLineHeight;
+    private int mProgressLineMargin;
+
+    private boolean mScrollStarted;
+    private float mTotalScrollDistance;
+
+    private int mMiddleLineColor;
+
+    public HorizontalProgressWheelView(Context context) {
+        this(context, null);
+    }
+
+    public HorizontalProgressWheelView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public HorizontalProgressWheelView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init();
+    }
+
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    public HorizontalProgressWheelView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    public void setScrollingListener(ScrollingListener scrollingListener) {
+        mScrollingListener = scrollingListener;
+    }
+
+    public void setMiddleLineColor(@ColorInt int middleLineColor) {
+        mMiddleLineColor = middleLineColor;
+        invalidate();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mLastTouchedPosition = event.getX();
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mScrollingListener != null) {
+                    mScrollStarted = false;
+                    mScrollingListener.onScrollEnd();
+                }
+                break;
+            case MotionEvent.ACTION_MOVE:
+                float distance = event.getX() - mLastTouchedPosition;
+                if (distance != 0) {
+                    if (!mScrollStarted) {
+                        mScrollStarted = true;
+                        if (mScrollingListener != null) {
+                            mScrollingListener.onScrollStart();
+                        }
+                    }
+                    onScrollEvent(event, distance);
+                }
+                break;
+        }
+        return true;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        canvas.getClipBounds(mCanvasClipBounds);
+
+        int linesCount = mCanvasClipBounds.width() / (mProgressLineWidth + mProgressLineMargin);
+        float deltaX = (mTotalScrollDistance) % (float) (mProgressLineMargin + mProgressLineWidth);
+
+        mProgressLinePaint.setColor(getResources().getColor(R.color.ucrop_color_progress_wheel_line));
+        for (int i = 0; i < linesCount; i++) {
+            if (i < (linesCount / 4)) {
+                mProgressLinePaint.setAlpha((int) (255 * (i / (float) (linesCount / 4))));
+            } else if (i > (linesCount * 3 / 4)) {
+                mProgressLinePaint.setAlpha((int) (255 * ((linesCount - i) / (float) (linesCount / 4))));
+            } else {
+                mProgressLinePaint.setAlpha(255);
+            }
+            canvas.drawLine(
+                    -deltaX + mCanvasClipBounds.left + i * (mProgressLineWidth + mProgressLineMargin),
+                    mCanvasClipBounds.centerY() - mProgressLineHeight / 4.0f,
+                    -deltaX + mCanvasClipBounds.left + i * (mProgressLineWidth + mProgressLineMargin),
+                    mCanvasClipBounds.centerY() + mProgressLineHeight / 4.0f, mProgressLinePaint);
+        }
+
+        mProgressLinePaint.setColor(mMiddleLineColor);
+        canvas.drawLine(mCanvasClipBounds.centerX(), mCanvasClipBounds.centerY() - mProgressLineHeight / 2.0f, mCanvasClipBounds.centerX(), mCanvasClipBounds.centerY() + mProgressLineHeight / 2.0f, mProgressLinePaint);
+
+    }
+
+    private void onScrollEvent(MotionEvent event, float distance) {
+        mTotalScrollDistance -= distance;
+        postInvalidate();
+        mLastTouchedPosition = event.getX();
+        if (mScrollingListener != null) {
+            mScrollingListener.onScroll(-distance, mTotalScrollDistance);
+        }
+    }
+
+    private void init() {
+        mMiddleLineColor = ContextCompat.getColor(getContext(), R.color.ucrop_color_progress_wheel_line);
+
+        mProgressLineWidth = getContext().getResources().getDimensionPixelSize(R.dimen.ucrop_width_horizontal_wheel_progress_line);
+        mProgressLineHeight = getContext().getResources().getDimensionPixelSize(R.dimen.ucrop_height_horizontal_wheel_progress_line);
+        mProgressLineMargin = getContext().getResources().getDimensionPixelSize(R.dimen.ucrop_margin_horizontal_wheel_progress_line);
+
+        mProgressLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mProgressLinePaint.setStyle(Paint.Style.STROKE);
+        mProgressLinePaint.setStrokeWidth(mProgressLineWidth);
+
+    }
+
+    public interface ScrollingListener {
+
+        void onScrollStart();
+
+        void onScroll(float delta, float totalDistance);
+
+        void onScrollEnd();
+    }
+
+}

+ 14 - 0
ucrop/src/main/jni/Android.mk

@@ -0,0 +1,14 @@
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE    := ucrop
+LOCAL_SRC_FILES := uCrop.cpp
+
+LOCAL_LDLIBS    := -landroid -llog -lz
+LOCAL_STATIC_LIBRARIES := libpng libjpeg_static
+
+include $(BUILD_SHARED_LIBRARY)
+
+$(call import-module,libpng)
+$(call import-module,libjpeg)

+ 6 - 0
ucrop/src/main/jni/Application.mk

@@ -0,0 +1,6 @@
+APP_STL := gnustl_static
+APP_ABI := armeabi armeabi-v7a x86 x86_64 arm64-v8a
+APP_CPPFLAGS += -frtti
+APP_CPPFLAGS += -fexceptions
+APP_CPPFLAGS += -DANDROID
+APP_PLATFORM := android-14

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 56164 - 0
ucrop/src/main/jni/CImg.h


+ 22 - 0
ucrop/src/main/jni/com_yalantis_ucrop_task_BitmapCropTask.h

@@ -0,0 +1,22 @@
+/* DO NOT EDIT THIS FILE - it is machine generated */
+#include <jni.h>
+/* Header for class com_yalantis_ucrop_task_BitmapCropTask */
+
+#ifndef _Included_com_yalantis_ucrop_task_BitmapCropTask
+#define _Included_com_yalantis_ucrop_task_BitmapCropTask
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+* Class:     com_yalantis_ucrop_task_BitmapCropTask
+* Method:    cropCImg
+* Signature: (Ljava/lang/String;Ljava/lang/String;IIIIF)Z
+*/
+JNIEXPORT jboolean JNICALL Java_com_yalantis_ucrop_task_BitmapCropTask_cropCImg
+(JNIEnv *, jobject, jstring, jstring, jint, jint, jint, jint, jfloat, jfloat, jint, jint, jint, jint);
+
+#ifdef __cplusplus
+}
+#endif
+#endif

+ 116 - 0
ucrop/src/main/jni/uCrop.cpp

@@ -0,0 +1,116 @@
+//
+// Created by Oleksii Shliama on 3/13/16.
+//
+
+#include <stdio.h>
+#include <jni.h>
+#include <vector>
+#include <android/log.h>
+#include "com_yalantis_ucrop_task_BitmapCropTask.h"
+
+using namespace std;
+
+#define cimg_display 0
+#define cimg_use_jpeg
+#define cimg_use_png
+#define cimg_use_openmp
+
+#include "CImg.h"
+
+using namespace cimg_library;
+
+#define LOG_TAG "uCrop JNI"
+#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
+#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
+
+#define SAVE_FORMAT_JPEG 0
+#define SAVE_FORMAT_PNG  1
+
+JNIEXPORT jboolean JNICALL Java_com_yalantis_ucrop_task_BitmapCropTask_cropCImg
+    (JNIEnv *env, jobject obj,
+    jstring pathSource, jstring pathResult,
+    jint left, jint top, jint width, jint height, jfloat angle, jfloat resizeScale,
+    jint format, jint quality,
+    jint exifDegrees, jint exifTranslation) {
+
+    LOGD("Crop image with CImg");
+
+    const char *file_source_path = env->GetStringUTFChars(pathSource, 0);
+    const char *file_result_path = env->GetStringUTFChars(pathResult, 0);
+
+    try {
+        CImg<unsigned char> img(file_source_path);
+        const int
+        x0 = left, y0 = top,
+        x1 = left + width - 1, y1 = top + height - 1;
+
+        /*
+        LOGD("left %d\ntop: %d", left, top);
+        LOGD("width %d\nheight: %d", width, height);
+        LOGD("angle %f\nresizeScale: %f", angle, resizeScale);
+        LOGD("image size pre: %d x %d", img.width(), img.height());
+        LOGD("exifDegrees: %d \nexifTranslation: %d", exifDegrees, exifTranslation);
+        */
+
+        // Handle exif. However it is slow, maybe calculate warp field according to exif rotation/translation.
+        if (exifDegrees != 0) {
+            img.rotate(exifDegrees);
+        }
+        if (exifTranslation != 1) {
+            img.mirror("x");
+        }
+
+        const int
+            size_x = img.width() * resizeScale, size_y = img.height() * resizeScale,
+            size_z = -100, size_c = -100, interpolation_type = 1;
+
+        const unsigned int boundary_conditions = 0;
+        const float
+        centering_x = 0, centering_y = 0, centering_z = 0, centering_c = 0;
+        if (resizeScale != 1) {
+            img.resize(size_x, size_y, size_z, size_c, interpolation_type, boundary_conditions, centering_x, centering_y, centering_z, centering_c);
+        }
+
+        // Create warp field.
+        CImg<float> warp(cimg::abs(x1 - x0 + 1), cimg::abs(y1 - y0 + 1), 1, 2);
+
+        const float
+        rad = angle * cimg::PI/180,
+        ca = std::cos(rad), sa = std::sin(rad),
+        ux = cimg::abs(img.width() * ca), uy = cimg::abs(img.width() * sa),
+        vx = cimg::abs(img.height() * sa), vy = cimg::abs(img.height() * ca),
+        w2 = 0.5f * img.width(), h2 = 0.5f * img.height(),
+        dw2 = 0.5f * (ux + vx), dh2 = 0.5f * (uy + vy);
+
+        cimg_forXY(warp, x, y) {
+            const float
+            u = x + x0 - dw2, v = y + y0 - dh2;
+
+            warp(x, y, 0) = w2 + u*ca + v*sa;
+            warp(x, y, 1) = h2 - u*sa + v*ca;
+        }
+
+        img = img.get_warp(warp, 0, 1, 2);
+
+        if (format == SAVE_FORMAT_JPEG) {
+            img.save_jpeg(file_result_path, quality);
+        } else if (format == SAVE_FORMAT_PNG) {
+            img.save_png(file_result_path, 0);
+        } else {
+            img.save(file_result_path);
+        }
+
+        ~img;
+        env->ReleaseStringUTFChars(pathSource, file_source_path);
+        env->ReleaseStringUTFChars(pathResult, file_result_path);
+
+        return true;
+
+    } catch (CImgInstanceException e) {
+        env->ThrowNew(env->FindClass("java/lang/OutOfMemoryError"), e.what());
+    } catch (CImgIOException e) {
+        env->ThrowNew(env->FindClass("java/io/IOException"), e.what());
+    }
+
+    return false;
+}

BIN
ucrop/src/main/jniLibs/arm64-v8a/libucrop.so


BIN
ucrop/src/main/jniLibs/armeabi-v7a/libucrop.so


BIN
ucrop/src/main/jniLibs/armeabi/libucrop.so


BIN
ucrop/src/main/jniLibs/x86/libucrop.so


BIN
ucrop/src/main/jniLibs/x86_64/libucrop.so


+ 20 - 0
ucrop/src/main/res/anim/ucrop_loader_circle_path.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <objectAnimator
+        android:duration="@integer/ucrop_progress_loading_anim_time"
+        android:propertyName="trimPathEnd"
+        android:repeatCount="-1"
+        android:valueFrom="0"
+        android:valueTo="1"
+        android:valueType="floatType"/>
+
+    <objectAnimator
+        android:duration="@integer/ucrop_progress_loading_anim_time"
+        android:propertyName="strokeAlpha"
+        android:repeatCount="-1"
+        android:valueFrom="1"
+        android:valueTo="0"
+        android:valueType="floatType"/>
+
+</set>

+ 28 - 0
ucrop/src/main/res/anim/ucrop_loader_circle_scale.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <objectAnimator
+        android:duration="@integer/ucrop_progress_loading_anim_time"
+        android:propertyName="scaleX"
+        android:repeatCount="-1"
+        android:valueFrom="0.666"
+        android:valueTo="1.0"
+        android:valueType="floatType"/>
+
+    <objectAnimator
+        android:duration="@integer/ucrop_progress_loading_anim_time"
+        android:propertyName="scaleY"
+        android:repeatCount="-1"
+        android:valueFrom="0.666"
+        android:valueTo="1.0"
+        android:valueType="floatType"/>
+
+    <objectAnimator
+        android:duration="@integer/ucrop_progress_loading_anim_time"
+        android:propertyName="rotation"
+        android:repeatCount="-1"
+        android:valueFrom="0"
+        android:valueTo="360"
+        android:valueType="floatType"/>
+
+</set>

+ 5 - 0
ucrop/src/main/res/color/ucrop_scale_text_view_selector.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="@color/ucrop_color_widget_active" android:state_selected="true"/>
+    <item android:color="@color/ucrop_color_widget"/>
+</selector>

BIN
ucrop/src/main/res/drawable-hdpi/ucrop_ic_angle.png


BIN
ucrop/src/main/res/drawable-hdpi/ucrop_ic_crop.png


BIN
ucrop/src/main/res/drawable-hdpi/ucrop_ic_cross.png


BIN
ucrop/src/main/res/drawable-hdpi/ucrop_ic_done.png


BIN
ucrop/src/main/res/drawable-hdpi/ucrop_ic_next.png


BIN
ucrop/src/main/res/drawable-hdpi/ucrop_ic_reset.png


BIN
ucrop/src/main/res/drawable-hdpi/ucrop_ic_rotate.png


BIN
ucrop/src/main/res/drawable-hdpi/ucrop_ic_scale.png


BIN
ucrop/src/main/res/drawable-ldpi/ucrop_ic_angle.png


BIN
ucrop/src/main/res/drawable-ldpi/ucrop_ic_crop.png


BIN
ucrop/src/main/res/drawable-ldpi/ucrop_ic_cross.png


+ 0 - 0
ucrop/src/main/res/drawable-ldpi/ucrop_ic_done.png


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels