[
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n"
  },
  {
    "path": ".idea/codeStyles/Project.xml",
    "content": "<component name=\"ProjectCodeStyleConfiguration\">\n  <code_scheme name=\"Project\" version=\"173\">\n    <Objective-C-extensions>\n      <file>\n        <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Import\" />\n        <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Macro\" />\n        <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Typedef\" />\n        <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Enum\" />\n        <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Constant\" />\n        <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Global\" />\n        <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Struct\" />\n        <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"FunctionPredecl\" />\n        <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Function\" />\n      </file>\n      <class>\n        <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Property\" />\n        <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Synthesize\" />\n        <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"InitMethod\" />\n        <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"StaticMethod\" />\n        <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"InstanceMethod\" />\n        <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"DeallocMethod\" />\n      </class>\n      <extensions>\n        <pair source=\"cpp\" header=\"h\" fileNamingConvention=\"NONE\" />\n        <pair source=\"c\" header=\"h\" fileNamingConvention=\"NONE\" />\n      </extensions>\n    </Objective-C-extensions>\n  </code_scheme>\n</component>"
  },
  {
    "path": ".idea/gradle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"GradleSettings\">\n    <option name=\"linkedExternalProjectsSettings\">\n      <GradleProjectSettings>\n        <option name=\"distributionType\" value=\"DEFAULT_WRAPPED\" />\n        <option name=\"externalProjectPath\" value=\"$PROJECT_DIR$\" />\n        <option name=\"modules\">\n          <set>\n            <option value=\"$PROJECT_DIR$\" />\n            <option value=\"$PROJECT_DIR$/app\" />\n          </set>\n        </option>\n        <option name=\"resolveModulePerSourceSet\" value=\"false\" />\n      </GradleProjectSettings>\n    </option>\n  </component>\n</project>"
  },
  {
    "path": ".idea/misc.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"NullableNotNullManager\">\n    <option name=\"myDefaultNullable\" value=\"android.support.annotation.Nullable\" />\n    <option name=\"myDefaultNotNull\" value=\"android.support.annotation.NonNull\" />\n    <option name=\"myNullables\">\n      <value>\n        <list size=\"5\">\n          <item index=\"0\" class=\"java.lang.String\" itemvalue=\"org.jetbrains.annotations.Nullable\" />\n          <item index=\"1\" class=\"java.lang.String\" itemvalue=\"javax.annotation.Nullable\" />\n          <item index=\"2\" class=\"java.lang.String\" itemvalue=\"javax.annotation.CheckForNull\" />\n          <item index=\"3\" class=\"java.lang.String\" itemvalue=\"edu.umd.cs.findbugs.annotations.Nullable\" />\n          <item index=\"4\" class=\"java.lang.String\" itemvalue=\"android.support.annotation.Nullable\" />\n        </list>\n      </value>\n    </option>\n    <option name=\"myNotNulls\">\n      <value>\n        <list size=\"4\">\n          <item index=\"0\" class=\"java.lang.String\" itemvalue=\"org.jetbrains.annotations.NotNull\" />\n          <item index=\"1\" class=\"java.lang.String\" itemvalue=\"javax.annotation.Nonnull\" />\n          <item index=\"2\" class=\"java.lang.String\" itemvalue=\"edu.umd.cs.findbugs.annotations.NonNull\" />\n          <item index=\"3\" class=\"java.lang.String\" itemvalue=\"android.support.annotation.NonNull\" />\n        </list>\n      </value>\n    </option>\n  </component>\n  <component name=\"ProjectRootManager\" version=\"2\" languageLevel=\"JDK_1_7\" project-jdk-name=\"1.8\" project-jdk-type=\"JavaSDK\">\n    <output url=\"file://$PROJECT_DIR$/build/classes\" />\n  </component>\n  <component name=\"ProjectType\">\n    <option name=\"id\" value=\"Android\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/runConfigurations.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"RunConfigurationProducerService\">\n    <option name=\"ignoredProducers\">\n      <set>\n        <option value=\"org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer\" />\n        <option value=\"org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer\" />\n        <option value=\"org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer\" />\n      </set>\n    </option>\n  </component>\n</project>"
  },
  {
    "path": ".idea/vcs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping directory=\"$PROJECT_DIR$\" vcs=\"Git\" />\n  </component>\n</project>"
  },
  {
    "path": "README.md",
    "content": "\n### 第一站小红书图片裁剪控件，深度解析大厂炫酷控件\n\n先来看两张效果图：\n\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20190225141450923.gif)\n\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20190225141507355.gif)\n\n哈哈，就是这样了。效果差了一些，感兴趣的小伙伴们可以运行代码感受丝滑与弹性。前段时间在竞品小红书上看到了这样的效果：图片可以跟随手指移动，双指可以（无限）放大，缩小，还可以挤压，手指抬起后还有一个有趣的效果，图片回弹。。。一直想撸一个手势的控件，正好可以模仿小红书图片裁剪控件，话不多说，撸起袖子就是干。\n\n本系列共有两篇，在第二篇会重点讲解与RecyclerView的联动效果，先放一张效果图，感兴趣的小伙伴们继续关注哦：\n\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20190225151350892.gif)\n\n### []()初步分析\n\n先来看看小红书的样子： \n\n ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190225160645143.gif)\n \n ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190225160653576.gif)\n \n ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190225162314812.gif)\n \n emmmm，从效果上来看呢，其实也只是基本的Translation和Scale组合而已，难点在于缩小态下的阻尼计算，左下角那个按钮用来控制留白，填充等状态的切换（好像小红书还有bug，状态切换会导致图片位置不正确，哈哈哈），接下来我们就一步步分析，从而打造出属于我们的自己的效果。\n\n仔细观察，有没有发现：\n\n  * 单指滑动，图片跟随手指移动，当手指滑动到图片边缘继续沿同一方向滑动，会出现阻尼效果，滑动的距离越大，阻尼越大，手指抬起后，图片回弹到控件边缘；\n\n\n  * 双指触摸分两种情况，一种是双指向内挤压，图片缩小；另一种是双指向外扩散，图片放大；\n\n\n  * 当双指向外扩散达到一定的临界值，手指抬起后，图片缩小到临界值状态；\n\n\n  * 手指触摸且有一定的滑动值，会显示线条九宫格，且线条跟随图片的大小动态改变，始终分割图片为9等分，如果手指触摸停止，线条消失，再次滑动，线条则再次出现；\n\n那么图片缩放时，需要一个缩放中心点，也就是PivotX和PivotY，这个点默认情况下在View的中心。但很明显，它这个就不是在中心了，至于在哪里，先看下这张图：  \n![在这里插入图片描述](https://img-blog.csdnimg.cn/20190225141450923.gif)\n可以看到，图片始终是以双指的中点在缩放，那么缩放中心点就是双指连线的中点位置上了。又怎么获取到双指的中点坐标呢？这里涉及到了Android提供的两个帮助类：GestureDetector、ScaleGestureDetector。接下来让我们先来了解下这两个类，揭开它的神秘面纱。神秘？你个糟老头，坏得很，信你个鬼。。。\n\n### []()手势帮助类\n\n什么是手势帮助类？Android手机屏幕上，当我们触摸屏幕的时候，会产生许多手势事件，如down，up，scroll，filing等等。我们可以在onTouchEvent()方法里面完成各种手势识别。但是，我们自己去识别各种手势就比较麻烦了，而且有些情况可能考虑的不是那么的全面。所以，为了方便我们的使用Android就提供了GestureDetector帮助类，先来看看他的构造方法：\n\n```java\n    public GestureDetector(Context context, OnGestureListener listener, Handler handler,\n            boolean unused) {\n    }\n```\n\ncontext表示上下文，listener表示手势的监听回调，handler可以指定线程（UI线程、非UI线程），unused未被使用的参数。如果我们的手势不需要在子线程中处理，我们一般只关心前两个参数，context是上下文这个简单，重点看下listener参数：\n\nGestureDetector给我们提供了三个接口类与一个外部类：\n\n * OnGestureListener：接口，用来监听手势事件（6种）；\n\n * OnDoubleTapListener：接口，用来监听双击事件；\n\n * OnContextClickListener：接口，外接设备，比如外接鼠标产生的事件（本文中我们不考虑）；\n\n * SimpleOnGestureListener：外部类，SimpleOnGestureListener其实是上面三个接口中所有函数的集成，它包含了这三个接口里所有必须要实现的函数而且都已经重写，但所有方法体都是空的。需要自己根据情况去重写；\n\nOnGestureListener接口方法：\n\n```java\npublic interface OnGestureListener {\n        /**\n         * 按下。返回值表示事件是否处理\n         */\n        boolean onDown(MotionEvent e);\n        \n        /**\n         * 短按(手指尚未松开也没有达到scroll条件)\n         */\n        void onShowPress(MotionEvent e);\n\n        /**\n         * 轻触(手指松开)\n         */\n        boolean onSingleTapUp(MotionEvent e);\n\n        /**\n         * 滑动(一次完整的事件可能会多次触发该函数)。返回值表示事件是否处理\n         */\n        boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);\n\n        /**\n         * 长按(手指尚未松开也没有达到scroll条件)\n         */\n        void onLongPress(MotionEvent e);\n\n        /**\n         * 滑屏(用户按下触摸屏、快速滑动后松开，返回值表示事件是否处理)\n         */\n        boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);\n    }\n```\n\nOnDoubleTapListener接口方法：\n\n```java\n    public interface OnDoubleTapListener {\n        /**\n         * 单击事件(onSingleTapConfirmed，onDoubleTap是两个互斥的函数)\n         */\n        boolean onSingleTapConfirmed(MotionEvent e);\n\n        /**\n         * 双击事件\n         */\n        boolean onDoubleTap(MotionEvent e);\n\n        /**\n         * 双击事件产生之后手指还没有抬起的时候的后续事件\n         */\n        boolean onDoubleTapEvent(MotionEvent e);\n    }\n ```\n\nGestureDetector的使用：\n\n* 定义GestureDetector类；\n\n* 将touch事件交给GestureDetector（onTouchEvent函数里面调用GestureDetector的onTouchEvent函数）；\n\n* 处理SimpleOnGestureListener或者OnGestureListener、OnDoubleTapListener、OnContextClickListener三者之一的回调；\n\nGestureDetector使用流程如下（有关例子会在后文中讲到）：\n\n```java\n    public GestureView(Context context, @Nullable AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public GestureView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        // 第一步\n        mGestureDetector = new GestureDetector(context, mOnGestureListener);\n    }\n\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        // 第三步\n        return mGestureDetector.onTouchEvent(event);\n    }\n    //  第二步\n    GestureDetector.OnGestureListener mOnGestureListener = new GestureDetector.OnGestureListener() {\n        @Override\n        public boolean onDown(MotionEvent e) {\n            return false;\n        }\n```\n\n这里就不再深入GestureDetector源码讲解，有感兴趣的小伙伴可以自行查阅资料，接着了解ScaleGestureDetector缩放手势类，用法与GestureDetector类似，都是通过onTouchEvent()关联相应的MotionEvent事件。\n\nScaleGestureDetector类给提供了OnScaleGestureListener接口，来告诉我们缩放的过程中的一些回调：\n\n```java\n  public interface OnScaleGestureListener {\n        /**\n         * 缩放进行中，返回值表示是否下次缩放需要重置，如果返回ture，那么detector就会重置缩放事件，如果返回false，detector会在之前的缩放上继续进行计算\n         */\n        public boolean onScale(ScaleGestureDetector detector);\n\n        /**\n         * 缩放开始，返回值表示是否受理后续的缩放事件\n         */\n        public boolean onScaleBegin(ScaleGestureDetector detector);\n\n        /**\n         * 缩放结束\n         */\n        public void onScaleEnd(ScaleGestureDetector detector);\n    }\n```\n\nScaleGestureDetector类常用函数介绍，因为在缩放的过程中，要通过ScaleGestureDetector来获取一些缩放信息：\n\n```java\n    /**\n     * 缩放是否正处在进行中\n     */\n    public boolean isInProgress();\n\n    /**\n     * 返回组成缩放手势(两个手指)中点x的位置\n     */\n    public float getFocusX();\n\n    /**\n     * 返回组成缩放手势(两个手指)中点y的位置\n     */\n    public float getFocusY();\n\n    /**\n     * 组成缩放手势的两个触点的跨度(两个触点间的距离)\n     */\n    public float getCurrentSpan();\n\n    /**\n     * 同上，x的距离\n     */\n    public float getCurrentSpanX();\n\n    /**\n     * 同上，y的距离\n     */\n    public float getCurrentSpanY();\n\n    /**\n     * 组成缩放手势的两个触点的前一次缩放的跨度(两个触点间的距离)\n     */\n    public float getPreviousSpan();\n\n    /**\n     * 同上，x的距离\n     */\n    public float getPreviousSpanX();\n\n    /**\n     * 同上，y的距离\n     */\n    public float getPreviousSpanY();\n\n    /**\n     * 获取本次缩放事件的缩放因子,缩放事件以onScale()返回值为基准，一旦该方法返回true，代表本次事件结束，重新开启下次缩放事件。\n     */\n    public float getScaleFactor();\n\n    /**\n     * 返回上次缩放事件结束时到当前的时间间隔\n     */\n    public long getTimeDelta();\n\n    /**\n     * 获取当前motion事件的时间\n     */\n    public long getEventTime();\n```\n\nScaleGestureDetector使用方式与GestureDetector类似，这里就不再重复讲解，了解了相关手势类，接下来开始代码构思。\n\n### []()构思代码\n\n想一想，图片有任意尺寸，怎样才能让图片铺满控件，那么就需要对图片进行缩放，平移。还有一点是必须考虑的，在加载高分辨率的图片非常消耗内存，在低内存的手机上很容易造成OOM，那么针对高分辨率的图片就必须压缩。还有一种情况是来回切换相同的两张图片，如果每次都加载本地图片，既消耗内存速度还很慢，这时候缓存就很有必要了，第一次加载本地图片，再次切回到该图片加载缓存图片。\n\n显示图片，一般有两种方式，一种是Android提供了ImageView控件来显示图片；另一种直接在onDraw()方法里调用canvas.drawBitmap()方法，通过调研小红书显示方案，发现他采用了第二种：\n\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20190226170005627.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTI1NTEzNTA=,size_16,color_FFFFFF,t_70)\n(*^__^*) 嘻嘻……那我们就用第一种显示图片的方式，继承ImageView来显示图片。\n\n通过观察小红书，我们会发现：\n\n 1. 图片显示区域为宽高相等的矩形，那么在测量onMeasure的时候需要保证宽高一致，左下角小按钮的状态切换先不考虑，后面会重点讲解。\n \n 2. 图片默认会**充满整个控件并居中对齐**，那么怎么保证图片充满控件，最常规的做法就是：取控件的宽高与图片的宽高比的最大值缩放`Math.max(控件宽度/图片宽度,控件高度/图片高度)`；同理，取控件宽高与图片宽高的偏移量的一半来平移图片保证居中对齐。\n \n 3. 在2的基础上，非宽高相等的图片有一部分会显示在控件区域之外，可以通过手指滑动来显示，相信大家都用过[PhotoView](https://github.com/chrisbanes/PhotoView)，效果一致。 移动图片与移动控件的原理一样，都是改变setTranslation的值，不过这里用到了图片矩阵，通过改变Matrix.postTranslate(dx, dy)的值来移动图片。\n\n 4. 移动图片，那就不得不考虑越界问题，请观察下图，这里以上边界为例（左，右，下边界同理）。**注意：这里的越界指的不是数组越界，而是图片滑动到边缘继续沿相同方向滑动，图片未铺满控件区域。** 在下图中你会发现：图片跟随手指继续滑动，手指滑动的距离越大阻尼越大，手指抬起后图片会回弹到控件顶部。\n    ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190227104702363.gif)\n 5. 双指挤压图片缩小，扩散图片放大，缩放中心点是双指中点坐标，那么缩放比例怎么计算呢？最开始取的`缩放因子ScaleGestureDetector.getScaleFactor()` ，出来的效果真的天马行空（~~轻微挤压扩散图片无限放大缩小~~ ），接着给缩放因子加一个比例，效果依旧不行，哦豁。没办法，打印缩放数据，观察数据，寻找规律。几经尝试最后取了缩放因子的偏移量。~~为了写好控件，没什么捷径，只能多观察，多尝试。~~ 在缩小至越界的状态下，手指抬起，图片放大到充满控件；在放大到一定的阈值后放手后，图片回弹到一定的缩放比例。前文提到了在缩小至越界状态下单指滑动图片，根据四周滑动的距离，会出现阻尼效果，在后文会讲解阻尼算法。\n \n 6. 图片在滑动或缩放态下，会出现九宫格白色线条，线条始终平分控件内的图片为九等分，滑动或缩放停止线条消失，再次滑动或缩放线条出现，手指抬起后线条消失。\n\n嗯，整个过程的大致行为就是这样了。\n\n 开工写代码咯~\n\n### []()起名字\n\n在开始写代码之前，要先给这个自定义控件起一个名字，又哦豁。。。不会起名字，\n就叫：**裁剪图片控件(MCropImageView)** 吧。不要问我M字母是啥含义，我不会告诉你的。\n\n### []()编写代码\n\n#### []()宽高相等矩阵测量\n\n测量比较简单，具体请看相关代码：\n\n```java\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        int widthSize = MeasureSpec.getSize(widthMeasureSpec);\n        int heightSize = MeasureSpec.getSize(heightMeasureSpec);\n        if (widthSize > heightSize) {\n           // 取高\n            super.onMeasure(heightMeasureSpec, heightMeasureSpec);\n        } else {\n          // 取宽\n            super.onMeasure(widthMeasureSpec, widthMeasureSpec);\n        }\n    }\n```\n \n#### []()铺满居中\n\n铺满的原理上文已经讲到了，对应的公式如下：\n\n```java\n控件宽度/图片宽度 = a\n控件高度/高度高度 = b \nmBaseScale = Math.max(a,b)\nMatrix.postScale(mBaseScale, mBaseScale, 控件宽度/ 2, 控件高度/ 2)\n```\n\n居中的原理上面也提到过了，来看看代码怎么写：\n\n```java\n    @Override\n    public void onGlobalLayout() {\n        mMatrix.reset();\n        // 获取控件的宽度和高度\n        int viewWidth = getWidth();\n        int viewHeight = getHeight();\n\n        // 图片的固定宽度  高度\n        // 获取图片的宽度和高度\n        Drawable drawable = getDrawable();\n        if (null == drawable) {\n            return;\n        }\n        int drawableWidth = drawable.getIntrinsicWidth();\n        int drawableHeight = drawable.getIntrinsicHeight();\n\n        // 将图片移动到屏幕的中点位置\n        float dx = (viewWidth - drawableWidth) / 2;\n        float dy = (viewHeight - drawableHeight) / 2;\n        // 取最大值\n        mBaseScale = Math.max((float) viewWidth / drawableWidth, (float) viewHeight / drawableHeight);\n        // 平移居中\n        mMatrix.postTranslate(dx, dy);\n        // 缩放\n        mMatrix.postScale(mBaseScale, mBaseScale, viewWidth / 2, viewHeight / 2);\n        setImageMatrix(mMatrix);\n    }\n```\n\n有关Matrix的set 、 pre、post方法调用顺序，这里简单说一下（~~个人理解，有错还望指出~~ ），可以把Matrix的操作看成队列，**post方法添加到队列的尾部，pre添加到队列的头部，而set方法则重置队列**。\n\n看看铺满居中的效果：\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20190227163344347.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTI1NTEzNTA=,size_16,color_FFFFFF,t_70)\n#### []()单指滑动\n\n 单指滑动，在上文已经讲到GestureDetector.SimpleOnGestureListener内部接口用来处理手势滑动，重写以下接口方法：\n \n```java\n    // 处理手指滑动\n    private GestureDetector.SimpleOnGestureListener mSimpleOnGestureListener = new GestureDetector.SimpleOnGestureListener() {\n\n        @Override\n        public boolean onDown(MotionEvent e) {\n           // 消费事件\n            return true;\n        }\n\n        @Override\n        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {\n            // 限定单指\n            if (e1.getPointerCount() == e2.getPointerCount() && e1.getPointerCount() == 1) {\n               // distanceX 左正右负 所以这里取相反数\n                mMatrix.postTranslate(-distanceX, -distanceY);\n                setImageMatrix(mMatrix);\n                return true;\n            }\n            return super.onScroll(e1, e2, distanceX, distanceY);\n        }\n    };\n```\n\n获取到手指滑动的距离，对图片矩阵进行平移Matrix.postTranslate()，但在x轴方向获取到的滑动距离右负左正，y轴方向获取到的滑动距离上正下负，跟实际平移的值相反，那么平移值Matrix.postTranslate(-distanceX, -distanceY)取滑动距离的负数。\n\n单指滑动还有一个效果，越界下的阻尼效果，看看效果图：\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20190228094613336.gif)\n很明显图片跟随手指滑动，距离控件边缘越近，阻尼越大。那么很明显需要获取图片边缘距离控件的距离，然后根据滑动偏移量进行计算。为了获取图片边缘距离控件的距离，就需要获取图片的位置信息。那么怎样才能获取图片位置信息呢？\n\n在ViewGroup的transformPointToViewLocal方法中有这样一段代码：\n\n```java\n    if (!child.hasIdentityMatrix()) {\n        child.getInverseMatrix().mapPoints(point);\n    }\n```\n\n如果child所对应的矩阵发生过旋转、缩放等变化的话(补间动画不算，因为是临时的)，会通过矩阵的mapPoints方法来将触摸点转换到矩阵变换后的坐标。\n\n没错，我们也可以用矩阵的mapRect方法来将图片的坐标及尺寸转换一下，就像这样：\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20190228101509479.gif)\n这样就可以获取到图片的矩形区域，相关方法如下：\n\n```java\n    // 获取图片矩阵区域\n    private RectF getMatrixRectF() {\n        RectF rectF = new RectF();\n        Drawable drawable = getDrawable();\n        if (drawable != null) {\n            // 注意set\n            rectF.set(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());\n            mMatrix.mapRect(rectF);\n        }\n        return rectF;\n    }\n```\n\n获取到了图片矩阵，那么图片越界就很容易判定了，先看下面两张越界图：\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20190228125019321.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTI1NTEzNTA=,size_16,color_FFFFFF,t_70)![在这里插入图片描述](https://img-blog.csdnimg.cn/20190228125130665.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTI1NTEzNTA=,size_16,color_FFFFFF,t_70)\n图片上边缘距离控件顶部变量为topEdgeDistanceTop，左边缘距离控件左边变量为leftEdgeDistanceLeft，右边缘距离控件右边变量为rightEdgeDistanceRight，下边缘距离控件底部变量为bottomEdgeDistanceBottom，分别对应的代码如下：\n\n```java\n   // 获取图片矩阵\n   RectF rectF = getMatrixRectF();\n   float leftEdgeDistanceLeft = rectF.left;\n   float topEdgeDistanceTop = rectF.top;\n   //位移 rectF.right - rectF.left 图片宽度   \n   float rightEdgeDistanceRight = leftEdgeDistanceLeft + rectF.right - rectF.left - getWidth();\n   // rectF.bottom - rectF.top 图片高度\n   float bottomEdgeDistanceBottom = topEdgeDistanceTop + rectF.bottom - rectF.top - getHeight();\n```\n\n好了，这样就可以准确判定图片是否越界。接下来我们看看越界状态下的阻尼算法是怎么计算的，有什么规律：\n\n先来观察图片左右越界的情况（上下越界同理），左右越界又分为三种情况，**左越界&右不越界（简称左越界），右越界&左不越界（简称右越界），左越界&右越界（简称左右越界）** 左越界的情况与右越界类似，那么就只有两种情况：\n\n 1. 左越界\n    ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190228134422772.gif)\n\n可以看到在向左滑动的情况下，图片左侧距离控件左侧距离越大，阻力越大。通俗一点，手指滑动的距离越大，图片跟随手指滑动的距离就越小，那么可以根据以下公式获取阻尼系数：\n\n```java\n 最大阻尼数 / 最大偏移量 * leftEdgeDistanceLeft\n```\n\n最大阻尼数默认取值为9，最大偏移量为控件宽度的三分之一，对应的代码如下：\n\n```java\n   // 获取图片矩阵\n   RectF rectF = getMatrixRectF();\n   float leftEdgeDistanceLeft = rectF.left;\n   float rightEdgeDistanceRight = leftEdgeDistanceLeft + rectF.right - rectF.left - getWidth();\n   \n   // MAX_SCROLL_FACTOR = 3\n   int maxOffsetWidth = getWidth() / MAX_SCROLL_FACTOR;\n   int maxOffsetHeight = getHeight() / MAX_SCROLL_FACTOR;\n   // 图片左侧越界并且图片右侧未越界\n   if (leftEdgeDistanceLeft > 0 && rightEdgeDistanceRight > 0) {\n       // distanceX < 0 表示继续向右滑动\n       if (distanceX < 0) {\n           if (leftEdgeDistanceLeft < maxOffsetWidth) {\n               // DAMP_FACTOR = 9 系数越大阻尼越大  +1防止ratio为0\n               int ratio = (int) (DAMP_FACTOR / maxOffsetWidth * leftEdgeDistanceLeft) + 1;\n               distanceX /= ratio;\n           } else {\n               // 图片向右滑动超过了最大偏移量 图片则不平移\n               distanceX = 0;\n           }\n       }\n       // 向左滑动不做处理 默认取值distanceX\n   }\n```\n\n 2. 左右越界\n    ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190228143447916.gif)\n    \n左右越界的情况与左越界的情况正好相反，距离控件边缘越近，图片阻力越大。那么怎么判定图片距离控件边缘越近，这里分两种情况，图片中点在控件中点左侧以及图片中点在控件中点右侧。第一种情况图片中点在控件中点左侧，向左滑动阻力越大，向右滑动阻力为0；第二种情况图片中点在控件中点的右侧，向右滑动阻力越大，向左滑动阻力为0。\n\n来看看代码怎么写：\n\n```java\n    // 图片左侧越界并且图片右侧越界\n    if (leftEdgeDistanceLeft > 0 && rightEdgeDistanceRight < 0) {\n        // 控件宽度的一半\n        int halfWidth = getWidth() / 2;\n        // 获取图片中点x坐标\n        float centerX = (rectF.right - rectF.left) / 2 + rectF.left;\n        // 图片中点x坐标是否右侧偏移\n        boolean rightOffsetCenterX = centerX >= halfWidth;\n        // 右侧偏移并且向右滑动\n        if (distanceX < 0 && rightOffsetCenterX) {\n            // centerX - halfWidth 图片右侧偏移量\n            int ratio = (int) (DAMP_FACTOR / maxOffsetWidth * (centerX - halfWidth)) + 1;\n            distanceX /= ratio;\n        }\n        // 左侧偏移并且向左滑动\n        else if (distanceX > 0 && !rightOffsetCenterX) {\n            // halfWidth - centerX 左侧的偏移量\n            int ratio = (int) (DAMP_FACTOR / maxOffsetWidth * (halfWidth - centerX)) + 1;\n            distanceX /= ratio;\n        }\n    }\n```\n\n好了，左右越界就讲到这里，上下越界同理，越界的整体代码如下：\n\n```java\n        @Override\n        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {\n            if (e1.getPointerCount() == e2.getPointerCount() && e1.getPointerCount() == 1) {\n                // 获取图片矩阵\n                RectF rectF = getMatrixRectF();\n\n                float leftEdgeDistanceLeft = rectF.left;\n                float topEdgeDistanceTop = rectF.top;\n\n                float rightEdgeDistanceRight = leftEdgeDistanceLeft + rectF.right - rectF.left - getWidth();\n                float bottomEdgeDistanceBottom = topEdgeDistanceTop + rectF.bottom - rectF.top - getHeight();\n\n                // MAX_SCROLL_FACTOR = 3\n                int maxOffsetWidth = getWidth() / MAX_SCROLL_FACTOR;\n                int maxOffsetHeight = getHeight() / MAX_SCROLL_FACTOR;\n\n                // 图片左侧越界并且图片右侧未越界\n                if (leftEdgeDistanceLeft > 0 && rightEdgeDistanceRight > 0) {\n                    // distanceX < 0 表示继续向右滑动\n                    if (distanceX < 0) {\n                        if (leftEdgeDistanceLeft < maxOffsetWidth) {\n                            // DAMP_FACTOR = 9 系数越大阻尼越大  +1防止ratio为0\n                            int ratio = (int) (DAMP_FACTOR / maxOffsetWidth * leftEdgeDistanceLeft) + 1;\n                            distanceX /= ratio;\n                        } else {\n                            // 图片向右滑动超过了最大偏移量 图片则不平移\n                            distanceX = 0;\n                        }\n                    }\n                    // 向左滑动不做处理 默认取值distanceX\n                }\n                // 图片右侧越界并且图片左侧未越界 （同上处理）\n                else if (rightEdgeDistanceRight < 0 && leftEdgeDistanceLeft < 0) {\n                    // distanceX > 0 表示继续向左滑动\n                    if (distanceX > 0) {\n                        if (rightEdgeDistanceRight > -maxOffsetWidth) {\n                            int ratio = (int) (DAMP_FACTOR / maxOffsetWidth * -rightEdgeDistanceRight) + 1;\n                            distanceX /= ratio;\n                        } else {\n                            // 图片右侧距离控件右侧超过最大偏移量 图片则不平移\n                            distanceX = 0;\n                        }\n                    }\n                }\n                // 图片左侧越界并且图片右侧越界\n                else if (leftEdgeDistanceLeft > 0 && rightEdgeDistanceRight < 0) {\n                    // 控件宽度的一半\n                    int halfWidth = getWidth() / 2;\n                    // 获取图片中点x坐标\n                    float centerX = (rectF.right - rectF.left) / 2 + rectF.left;\n                    // 图片中点x坐标是否右侧偏移\n                    boolean rightOffsetCenterX = centerX >= halfWidth;\n                    // 右侧偏移并且向右滑动\n                    if (distanceX < 0 && rightOffsetCenterX) {\n                        // centerX - halfWidth 图片右侧偏移量\n                        int ratio = (int) (DAMP_FACTOR / maxOffsetWidth * (centerX - halfWidth)) + 1;\n                        distanceX /= ratio;\n                    }\n                    // 左侧偏移并且向左滑动\n                    else if (distanceX > 0 && !rightOffsetCenterX) {\n                        // halfWidth - centerX 左侧的偏移量\n                        int ratio = (int) (DAMP_FACTOR / maxOffsetWidth * (halfWidth - centerX)) + 1;\n                        distanceX /= ratio;\n                    }\n                }\n\n                // 上下越界 处理方式同左右处理方式一样 本可以提成一个方法但为了方便理解先这样了\n                // 图片上侧越界并且图片下侧未越界\n                if (topEdgeDistanceTop > 0 && bottomEdgeDistanceBottom > 0) {\n                    // distanceY < 0 表示图片继续向下滑动\n                    if (distanceY < 0) {\n                        if (topEdgeDistanceTop < maxOffsetHeight) {\n                            // 获取阻尼比例\n                            int ratio = (int) (DAMP_FACTOR / maxOffsetHeight * topEdgeDistanceTop) + 1;\n                            distanceY /= ratio;\n                        } else {\n                            // 向下滑动超过了最大偏移量 则图片不滑动\n                            distanceY = 0;\n                        }\n                    }\n                }\n                // 图片下侧越界并且图片上侧未越界\n                else if (bottomEdgeDistanceBottom < 0 && topEdgeDistanceTop < 0) {\n                    if (distanceY > 0) {\n                        if (bottomEdgeDistanceBottom > -maxOffsetHeight) {\n                            int ratio = (int) (DAMP_FACTOR / maxOffsetHeight * -bottomEdgeDistanceBottom) + 1;\n                            distanceY /= ratio;\n                        } else {\n                            // 向上滑动超过了最大偏移量 则图片不滑动\n                            distanceY = 0;\n                        }\n                    }\n                } else if (topEdgeDistanceTop > 0 && bottomEdgeDistanceBottom < 0) {\n                    int halfHeight = getHeight() / 2;\n                    // 获取图片中点y坐标\n                    float centerY = (rectF.bottom - rectF.top) / 2 + rectF.top;\n                    // 图片中点y坐标是否向下偏移\n                    boolean bottomOffsetCenterY = centerY >= halfHeight;\n                    // 向下偏移并且向下移动\n                    if (distanceY < 0 && bottomOffsetCenterY) {\n                        // centerY - halfHeight 图片偏移量\n                        int ratio = (int) (DAMP_FACTOR / maxOffsetHeight * (centerY - halfHeight)) + 1;\n                        distanceY /= ratio;\n                    } else if (distanceY > 0 && !bottomOffsetCenterY) { // 向上偏移并且向上移动\n                        int ratio = (int) (DAMP_FACTOR / maxOffsetHeight * (halfHeight - centerY)) + 1;\n                        distanceY /= ratio;\n                    }\n                }\n\n                mMatrix.postTranslate(-distanceX, -distanceY);\n                setImageMatrix(mMatrix);\n                return true;\n            }\n            return super.onScroll(e1, e2, distanceX, distanceY);\n        }\n```\n\n#### []()双指缩放\n\n双指缩放的原理在上文已经提及过了，重写ScaleGestureDetector.OnScaleGestureListener缩放手势类接口方法：\n\n```java\n    // 处理双指的缩放\n    private ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener = new ScaleGestureDetector.OnScaleGestureListener() {\n        @Override\n        public boolean onScale(ScaleGestureDetector detector) {\n            if (null == getDrawable() || mMatrix == null) {\n                // 如果返回true那么detector就会重置缩放事件\n                return true;\n            }\n            // 缩放因子,缩小小于1,放大大于1\n            float scaleFactor = mScaleGestureDetector.getScaleFactor();\n\n            // 缩放因子偏移量\n            float deltaFactor = scaleFactor - mPreScaleFactor;\n\n            if (scaleFactor != 1.0F && deltaFactor != 0F) {\n                mMatrix.postScale(deltaFactor + 1F, deltaFactor + 1F, mScaleGestureDetector.getFocusX(),\n                        mScaleGestureDetector.getFocusY());\n                setImageMatrix(mMatrix);\n            }\n            mPreScaleFactor = scaleFactor;\n            return false;\n        }\n\n        @Override\n        public boolean onScaleBegin(ScaleGestureDetector detector) {\n            // 注意返回true\n            return true;\n        }\n\n        @Override\n        public void onScaleEnd(ScaleGestureDetector detector) {\n        }\n    };\n```\n\n#### []()回弹\n\n在手指抬起时，图片在某种状态下会出现回弹动效，这里某种状态指的是**越界&图片的缩放比例大于一定的阈值&图片的缩放比例小于一定的阈值**三种状态，回弹无非改变图片矩阵的setTranslation，setScale值。当我们需要监听手指抬起的状态时，都是直接重写onTouchEvent去实现：\n\n```java\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        switch (event.getAction()) {\n            case MotionEvent.ACTION_DOWN:\n                // 防止父类拦截事件\n                getParent().requestDisallowInterceptTouchEvent(true);\n                break;\n            case MotionEvent.ACTION_MOVE:\n                break;\n            case MotionEvent.ACTION_CANCEL:\n            case MotionEvent.ACTION_UP:\n                float scale = getScale();\n                if (scale > mMaxScale) {\n                    // 缩小\n                } else if (scale < mBaseScale) {\n                    // 放大\n                } else {\n                    // 平移\n                }\n               getParent().requestDisallowInterceptTouchEvent(false);\n                break;\n        }\n        return true;\n    }\n```\n\n~~为了防止父类拦截事件，一般会在手指按下，抬起调用requestDisallowInterceptTouchEvent方法来避免事件冲突。~~ getScale方法如下，获取图片矩阵的缩放比例：\n\n```java\n    private float getScale() {\n        float[] values = new float[9];\n        mMatrix.getValues(values);\n        return values[Matrix.MSCALE_X];\n    }\n```\n\n缩小放大的动画怎么实现呢？知道了开始与结束的缩放比例，在动画回调接口中动态设置 mMatrix.setValues(values)来实现缩小放大的效果，可现实很骨感，效果相去甚远，缩放中心点PivotX和PivotY始终在图片原点，同时Matrix并没有提供设置缩放中心点的方法。看来只能老老实实的使用Matrix.postScale(float sx, float sy, float px, float py)方法，同时设置缩放中心点为双指的中点坐标ScaleGestureDetector.getFocusX()。注意：**sx，sx是相对值，相对上一个终点的缩放值。**\n\n相对值，多缩放一次与少缩放一次图片的状态完全不一样，那么必须控制缩放次数，由于ValueAnimator回调次数在不同的机型上并不一样，那么就不能用ValueAnimator的回调来实现动画，那么怎么做呢？\n\nemmmm，你一定会想到Handler，既可以控制次数还可以控制消息延时。知道了开始与结束缩放点，也知道了缩放次数，那么怎么获取缩放相对值呢，利用Math.pow数学公式：\n\n```java\n      /**\n     * 计算d的1/count次幂\n     *\n     * @param d\n     * @param count 开根的次数\n     * @return 相对值\n     */\n    private static float getRelativeValue(double d, double count) {\n        if (count == 0) {\n            return 1F;\n        }\n        count = 1 / count;\n        return (float) Math.pow(d, count);\n    }\n```\n\n接下来就是发送消息与接收消息：\n\n```java\n    /**\n     * 发送消息\n     *\n     * @param relativeScale\n     * @param what\n     * @param delayMillis\n     */\n    private void sendMessage(float relativeScale, int what, long delayMillis) {\n        Message mes = new Message();\n        mes.obj = relativeScale;\n        mes.what = what;\n        mHandler.sendMessageDelayed(mes, delayMillis);\n    }\n   \n   // 调用 省略前面 ...   \n    case MotionEvent.ACTION_UP:\n       float scale = getScale();\n       if (scale > mMaxScale) {\n           // 缩小 SCALE_ANIM_COUNT = 10  ZOOM_OUT_ANIM_WHIT = 0 \n           sendMessage(getRelativeValue(mMaxScale / scale, SCALE_ANIM_COUNT), ZOOM_OUT_ANIM_WHIT, 0);\n       } else if (scale < mBaseScale) {\n           // 放大 ZOOM_ANIM_WHIT = 1 \n           sendMessage(getRelativeValue(mMaxScale / scale, SCALE_ANIM_COUNT), ZOOM_ANIM_WHIT, 0);\n       } else {\n           // 平移\n           boundCheck();\n       }\n```\n\n接收并处理消息：\n\n```java\n    private Handler mHandler = new Handler() {\n        @Override\n        public void handleMessage(Message msg) {\n            super.handleMessage(msg);\n            if (msg != null) {\n                if (mCurrentScaleAnimCount < SCALE_ANIM_COUNT) {\n                    float obj = (float) msg.obj;\n                    mMatrix.postScale(obj, obj, mLastFocusX, mLastFocusY);\n                    setImageMatrix(mMatrix);\n                    mCurrentScaleAnimCount++;\n                    // what scale > mMaxScale 取0 不然取 1\n                    sendScaleMessage(obj, msg.what, SCALE_ANIM_COUNT);\n                } else if (mCurrentScaleAnimCount >= SCALE_ANIM_COUNT) {\n                    float[] values = new float[9];\n                    mMatrix.getValues(values);\n                    if (msg.what == ZOOM_OUT_ANIM_WHIT) {\n                        values[Matrix.MSCALE_X] = mMaxScale;\n                        values[Matrix.MSCALE_Y] = mMaxScale;\n                    } else if (msg.what == ZOOM_ANIM_WHIT) {\n                        values[Matrix.MSCALE_X] = mBaseScale;\n                        values[Matrix.MSCALE_Y] = mBaseScale;\n                    }\n                    mMatrix.setValues(values);\n                    setImageMatrix(mMatrix);\n\n                    // 边界检测\n                    boundCheck();\n                }\n            }\n        }\n    };\n```\n\n缩小放大的效果如下：\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20190228201850548.gif)![在这里插入图片描述](https://img-blog.csdnimg.cn/20190228201858592.gif)\n为了防止Handler泄露，清空队列：\n\n```java\n    @Override\n    protected void onDetachedFromWindow() {\n        if (mHandler != null) {\n            // 防止内存泄露\n            mHandler.removeCallbacksAndMessages(null);\n        }\n        super.onDetachedFromWindow();\n    }\n```\n\n回弹还剩最后一种情况越界，在上文中已经提到了越界的四种（上下左右）情况，手指抬起后图片平移到控件边缘。所谓的平移，就是从一点平移到另一点，那么怎么获取起点与结束点呢？\n\n首先需要判定越界，根据getMatrixRectF图片矩阵，代码已经很清晰：\n\n```java\n    // 边界检测\n    private void boundCheck() {\n        // 获取图片矩阵\n        RectF rectF = getMatrixRectF();\n\n        if (rectF.left >= 0) {\n            // 左越界\n        }\n\n        if (rectF.top >= 0) {\n            // 上越界\n        }\n\n        if (rectF.right <= getWidth()) {\n            // 右越界\n        }\n\n        if (rectF.bottom <= getHeight()) {\n            // 下越界\n        }\n    }\n```\n\n在左越界的情况下，起点为rectF.left，结束点为0；同理上越界的起点rectF.top，结束点0；那么右越界起点与结束点呢？有小伙伴会说那还不简单，不就是rectF.right，getWidth()吗？\n\n很遗憾，你又哦豁了，不得不提一下，图片的矩阵的平移是以左上角为基点，那么右越界的起点同样为rectF.left，结束点为：\n\n```java\n    起点 + 图片右侧距离控件右侧的距离\n```\n\n图片右侧距离控件右侧的距离为getWidth() - rectF.right，那么结束点的坐标为rectF.left + getWidth() - rectF.right；同理下越界的起点为rectF.top，结束点getHeight() - rectF.bottom + rectF.top。有了起点与结束点，那么平移就很容易了：\n\n```java\n    /**\n     * 开始越界动画\n     *\n     * @param start      开始点坐标\n     * @param end        结束点坐标\n     * @param horizontal 是否水平动画  true 水平动画 false 垂直动画\n     */\n    private void startBoundAnimator(float start, float end, final boolean horizontal) {\n        boundAnimator = ValueAnimator.ofFloat(start, end);\n        boundAnimator.setDuration(200);\n        boundAnimator.setInterpolator(new LinearInterpolator());\n        boundAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n            @Override\n            public void onAnimationUpdate(ValueAnimator animation) {\n                float v = (float) animation.getAnimatedValue();\n\n                float[] values = new float[9];\n                mMatrix.getValues(values);\n                values[horizontal ? Matrix.MTRANS_X : Matrix.MTRANS_Y] = v;\n\n                mMatrix.setValues(values);\n                setImageMatrix(mMatrix);\n            }\n        });\n        boundAnimator.start();\n    }\n```\n\n好了，看看效果：\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20190301100531481.gif)\n#### []()九宫线条\n\n在上文已经提到九宫线条的规律： **图片在滑动或缩放态下，会出现九宫格白色线条，线条始终平分控件内的图片为九等分，滑动或缩放停止线条消失，再次滑动或缩放线条出现，手指抬起后线条消失**。那么从这句话中我们可以得出以下结论：\n\n 1. 有关绘制涉及到onDraw()方法的重写\n\n 2. 线条的显示区域为图片与控件的交集\n\n 3. 控制线条的显示与消失（是否绘制）\n\n怎么取交集记住一个原则：**上左取大，右下取小** 八字真言，就像这样：\n\n```java\n    // 开始点\n    float startX = 0;\n    float startY = 0;\n    // 结束点\n    float endX = 0;\n    float endY = 0;\n    RectF rectF = getMatrixRectF();\n    // 上左取大 右下取小\n    startX = rectF.left <= 0 ? 0 : rectF.left;\n    startY = rectF.top <= 0 ? 0 : rectF.top;\n    \n    endX = rectF.right >= getWidth() ? getWidth() : rectF.right;\n    endY = rectF.bottom >= getHeight() ? getHeight() : rectF.bottom;\n```\n\n获取到线条绘制的区域，那么怎么绘制线条？绘制多少线条？就比较容易了：\n\n```java\n        float lineWidth = 0;\n        float lineHeight = 0;\n\n        lineWidth = endX - startX;\n        lineHeight = endY - startY;\n\n        // LINE_ROW_NUMBER = 3 表示多少行\n        for (int i = 1; i < LINE_ROW_NUMBER; i++) {\n            canvas.drawLine(startX + 0, startY + lineHeight / LINE_ROW_NUMBER * i, endX, startY + lineHeight / LINE_ROW_NUMBER * i, mLinePaint);\n        }\n\n        // LINE_COLUMN_NUMBER = 3 表示多少列\n        for (int i = 1; i < LINE_COLUMN_NUMBER; i++) {\n            canvas.drawLine(startX + lineWidth / LINE_COLUMN_NUMBER * i, startY, startX + lineWidth / LINE_COLUMN_NUMBER * i, endY, mLinePaint);\n        }\n```\n\n怎么控制线条的显示消失，注意显示消失的规则，缩放或滑动停止线条消失，再次滑动或缩放线条显示，以此类推，绝大部分人会想到怎么判定滑动或缩放停止？\n\n写控件很多时候就是这样，不知不觉就入坑了，一头扎进里面，茶不思饭不想。。。然而这一切并没有什么用，最后还得换方案。\n\n说下为什么不行，你会在手势MotionEvent.ACTION_MOVE事件判定滑动或缩放停止，但同时GestureDetector与ScaleGestureDetector也在消费滑动事件，导致判定不准确。那么怎么解决呢？\n\n还记得Android源码长按事件的处理方式吗？相关代码如下：\n\n```java\ncase MotionEvent.ACTION_DOWN:\n        ......省略代码\n        if (mIsLongpressEnabled) {\n            mHandler.removeMessages(LONG_PRESS);\n            // 延迟时长为500毫秒\n            mHandler.sendEmptyMessageAtTime(LONG_PRESS,\n                    mCurrentDownEvent.getDownTime() + LONGPRESS_TIMEOUT);\n        }\n case MotionEvent.ACTION_MOVE:\n       int distance = (deltaX * deltaX) + (deltaY * deltaY);\n       int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare;\n       if (distance > slopSquare) {\n           ......省略代码\n           mHandler.removeMessages(LONG_PRESS);\n       }\n```\n\n在事件ACTION_DOWN延时发送长按事件，在延迟周期内，如果发生滑动，则移除长按事件，反之未发生滑动则触发长按事件。\n\n借鉴长按事件的处理方式：\n\n```java\n    // 绘制九宫线条\n    private void drawLine(Canvas canvas) {\n        // 省略中间代码\n        mHandler.removeCallbacks(lineRunnable);\n        mHandler.postDelayed(lineRunnable, 400);\n    }\n    \n    private Runnable lineRunnable = new Runnable() {\n        @Override\n        public void run() {\n            mIsDragging = false;\n            invalidate();\n        }\n    };\n    \n    @Override\n    protected void onDraw(Canvas canvas) {\n        super.onDraw(canvas);\n        if (mIsDragging) {\n            canvas.save();\n            drawLine(canvas);\n            canvas.restore();\n        }\n    }\n```\n\n效果就像这样：\n\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20190301191013730.gif)\n\n哈哈哈~，小红书的图片裁剪控件喜欢吗？想看更多炫酷控件，请搜索关注公众号：**控件人生**\n\n你可以留言，告诉小编想实现什么样的炫酷控件？小编会每周选取炫酷的控件进行讲解。\n\n由于篇幅原因，文章到这里就差不多了，有关左下角留白，填充效果，以及联动效果，将在下一篇讲解，打造属于你自己的CoordinatorLayout效果，喜欢的小伙伴被忘记关注控件人生（新公众号），同大家一起成长。\n\n### []()Github地址：https://github.com/HpWens/MCropImageView 欢迎Star\n### []()炫酷控件集：https://github.com/HpWens/MeiWidgetView 欢迎Star\n"
  },
  {
    "path": "app/.gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n\n"
  },
  {
    "path": "app/build.gradle",
    "content": "apply plugin: 'com.android.application'\n\nandroid {\n    compileSdkVersion 28\n    defaultConfig {\n        applicationId \"com.demo.mcropimageview\"\n        minSdkVersion 15\n        targetSdkVersion 28\n        versionCode 1\n        versionName \"1.0\"\n        testInstrumentationRunner \"android.support.test.runner.AndroidJUnitRunner\"\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation 'com.android.support:appcompat-v7:28.0.0'\n    implementation 'com.android.support.constraint:constraint-layout:1.1.3'\n    testImplementation 'junit:junit:4.12'\n    androidTestImplementation 'com.android.support.test:runner:1.0.2'\n    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'\n}\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "app/src/androidTest/java/com/demo/mcropimageview/ExampleInstrumentedTest.java",
    "content": "package com.demo.mcropimageview;\n\nimport android.content.Context;\nimport android.support.test.InstrumentationRegistry;\nimport android.support.test.runner.AndroidJUnit4;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\nimport static org.junit.Assert.*;\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\n@RunWith(AndroidJUnit4.class)\npublic class ExampleInstrumentedTest {\n    @Test\n    public void useAppContext() {\n        // Context of the app under test.\n        Context appContext = InstrumentationRegistry.getTargetContext();\n\n        assertEquals(\"com.demo.mcropimageview\", appContext.getPackageName());\n    }\n}\n"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.demo.mcropimageview\">\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/AppTheme\">\n        <activity android:name=\".MainActivity\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n    </application>\n\n</manifest>"
  },
  {
    "path": "app/src/main/java/com/demo/mcropimageview/MCropImageView.java",
    "content": "package com.demo.mcropimageview;\n\nimport android.animation.ValueAnimator;\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Matrix;\nimport android.graphics.Paint;\nimport android.graphics.RectF;\nimport android.graphics.drawable.Drawable;\nimport android.os.Build;\nimport android.os.Handler;\nimport android.os.Message;\nimport android.support.annotation.Nullable;\nimport android.support.v7.widget.AppCompatImageView;\nimport android.util.AttributeSet;\nimport android.view.GestureDetector;\nimport android.view.MotionEvent;\nimport android.view.ScaleGestureDetector;\nimport android.view.ViewConfiguration;\nimport android.view.ViewTreeObserver;\nimport android.view.animation.LinearInterpolator;\n\n/**\n * Created by wenshi on 2019/2/26.\n * Description\n */\npublic class MCropImageView extends AppCompatImageView implements ViewTreeObserver.OnGlobalLayoutListener {\n\n    // 手势帮助类\n    private GestureDetector mGestureDetector;\n    private ScaleGestureDetector mScaleGestureDetector;\n\n    private boolean mFirstLayout;\n    private float mBaseScale = 1.0F;\n    private float mMaxScale = 3.0F;\n\n    private float mPreScaleFactor = 1.0f;\n    private Matrix mMatrix;\n\n    // 缩放手势(两个手指)中点位置\n    private float mLastFocusX;\n    private float mLastFocusY;\n\n    private int mTouchSlop = -1;\n\n    private int mCurrentScaleAnimCount;\n    private ValueAnimator boundAnimator;\n\n    private Paint mLinePaint;\n    // 是否绘制线条\n    private boolean mIsDragging;\n\n    private static int MAX_SCROLL_FACTOR = 3;\n    // 阻尼系数\n    private static float DAMP_FACTOR = 9.0F;\n\n    private static int SCALE_ANIM_COUNT = 10;\n    private static int ZOOM_OUT_ANIM_WHIT = 0;\n    private static int ZOOM_ANIM_WHIT = 1;\n    private static int LINE_ROW_NUMBER = 3;\n    private static int LINE_COLUMN_NUMBER = 3;\n\n    public MCropImageView(Context context) {\n        this(context, null);\n    }\n\n    public MCropImageView(Context context, @Nullable AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public MCropImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n\n        mScaleGestureDetector = new ScaleGestureDetector(context, mOnScaleGestureListener);\n        mGestureDetector = new GestureDetector(context, mSimpleOnGestureListener);\n\n        mFirstLayout = true;\n        mMatrix = new Matrix();\n        setScaleType(ScaleType.MATRIX);\n\n        mLinePaint = new Paint();\n        mLinePaint.setAntiAlias(true);\n        mLinePaint.setColor(Color.WHITE);\n        mLinePaint.setStrokeWidth(dip2px(context, 0.5f));\n    }\n\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        if (mTouchSlop < 0) {\n            mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();\n        }\n        switch (event.getAction()) {\n            case MotionEvent.ACTION_DOWN:\n                // 防止父类拦截事件\n                getParent().requestDisallowInterceptTouchEvent(true);\n                mIsDragging = false;\n                break;\n            case MotionEvent.ACTION_MOVE:\n                break;\n            case MotionEvent.ACTION_CANCEL:\n            case MotionEvent.ACTION_UP:\n                mCurrentScaleAnimCount = 0;\n                mIsDragging = false;\n                invalidate();\n                float scale = getScale();\n                if (scale > mMaxScale) {\n                    // 缩小 SCALE_ANIM_COUNT = 10\n                    sendScaleMessage(getRelativeValue(mMaxScale / scale, SCALE_ANIM_COUNT), ZOOM_OUT_ANIM_WHIT, 0);\n                } else if (scale < mBaseScale) {\n                    // 放大\n                    sendScaleMessage(getRelativeValue(mBaseScale / scale, SCALE_ANIM_COUNT), ZOOM_ANIM_WHIT, 0);\n                } else {\n                    // 平移\n                    boundCheck();\n                }\n                getParent().requestDisallowInterceptTouchEvent(true);\n                break;\n        }\n        if (mGestureDetector.onTouchEvent(event)) {\n            return true;\n        }\n        mScaleGestureDetector.onTouchEvent(event);\n        return true;\n    }\n\n    // 边界检测\n    private void boundCheck() {\n        // 获取图片矩阵\n        RectF rectF = getMatrixRectF();\n\n        if (rectF.left >= 0) {\n            // 左越界\n            startBoundAnimator(rectF.left, 0, true);\n        }\n\n        if (rectF.top >= 0) {\n            // 上越界\n            startBoundAnimator(rectF.top, 0, false);\n        }\n\n        if (rectF.right <= getWidth()) {\n            // 右越界\n            startBoundAnimator(rectF.left, rectF.left + getWidth() - rectF.right, true);\n        }\n\n        if (rectF.bottom <= getHeight()) {\n            // 下越界\n            startBoundAnimator(rectF.top, getHeight() - rectF.bottom + rectF.top, false);\n        }\n    }\n\n    /**\n     * 开始越界动画\n     *\n     * @param start      开始点坐标\n     * @param end        结束点坐标\n     * @param horizontal 是否水平动画  true 水平动画 false 垂直动画\n     */\n    private void startBoundAnimator(float start, float end, final boolean horizontal) {\n        boundAnimator = ValueAnimator.ofFloat(start, end);\n        boundAnimator.setDuration(200);\n        boundAnimator.setInterpolator(new LinearInterpolator());\n        boundAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n            @Override\n            public void onAnimationUpdate(ValueAnimator animation) {\n                float v = (float) animation.getAnimatedValue();\n\n                float[] values = new float[9];\n                mMatrix.getValues(values);\n                values[horizontal ? Matrix.MTRANS_X : Matrix.MTRANS_Y] = v;\n\n                mMatrix.setValues(values);\n                setImageMatrix(mMatrix);\n            }\n        });\n        boundAnimator.start();\n    }\n\n\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        int widthSize = MeasureSpec.getSize(widthMeasureSpec);\n        int heightSize = MeasureSpec.getSize(heightMeasureSpec);\n        if (widthSize > heightSize) {\n            super.onMeasure(heightMeasureSpec, heightMeasureSpec);\n        } else {\n            super.onMeasure(widthMeasureSpec, widthMeasureSpec);\n        }\n    }\n\n\n    @Override\n    public void onGlobalLayout() {\n        if (mFirstLayout) {\n            mFirstLayout = false;\n            mMatrix.reset();\n            // 获取控件的宽度和高度\n            int viewWidth = getWidth();\n            int viewHeight = getHeight();\n\n            // 图片的固定宽度  高度\n            // 获取图片的宽度和高度\n            Drawable drawable = getDrawable();\n            if (null == drawable) {\n                return;\n            }\n            int drawableWidth = drawable.getIntrinsicWidth();\n            int drawableHeight = drawable.getIntrinsicHeight();\n\n            // 将图片移动到屏幕的中点位置\n            float dx = (viewWidth - drawableWidth) / 2;\n            float dy = (viewHeight - drawableHeight) / 2;\n\n            mBaseScale = Math.max((float) viewWidth / drawableWidth, (float) viewHeight / drawableHeight);\n            // 平移居中\n            mMatrix.postTranslate(dx, dy);\n            // 缩放\n            mMatrix.postScale(mBaseScale, mBaseScale, viewWidth / 2, viewHeight / 2);\n            setImageMatrix(mMatrix);\n\n            if (mBaseScale >= mMaxScale) {\n                mMaxScale = (int) Math.floor(mBaseScale) + 2;\n            } else if (mBaseScale < 1.0f) {\n                mMaxScale = 1.0f;\n            }\n        }\n    }\n\n    @Override\n    protected void onAttachedToWindow() {\n        super.onAttachedToWindow();\n        getViewTreeObserver().addOnGlobalLayoutListener(this);\n    }\n\n    @Override\n    protected void onDetachedFromWindow() {\n        if (mHandler != null) {\n            // 防止内存泄露\n            mHandler.removeCallbacksAndMessages(null);\n        }\n        super.onDetachedFromWindow();\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {\n            getViewTreeObserver().removeOnGlobalLayoutListener(this);\n        } else {\n            getViewTreeObserver().removeGlobalOnLayoutListener(this);\n        }\n    }\n\n    // 获取图片矩阵区域\n    private RectF getMatrixRectF() {\n        RectF rectF = new RectF();\n        Drawable drawable = getDrawable();\n        if (drawable != null) {\n            rectF.set(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());\n            mMatrix.mapRect(rectF);\n        }\n        return rectF;\n    }\n\n    // 处理双指的缩放\n    private ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener = new ScaleGestureDetector.OnScaleGestureListener() {\n        @Override\n        public boolean onScale(ScaleGestureDetector detector) {\n            if (null == getDrawable() || mMatrix == null) {\n                // 如果返回true那么detector就会重置缩放事件\n                return true;\n            }\n            mIsDragging = true;\n            // 缩放因子,缩小小于1,放大大于1\n            float scaleFactor = mScaleGestureDetector.getScaleFactor();\n\n            // 缩放因子偏移量\n            float deltaFactor = scaleFactor - mPreScaleFactor;\n\n            if (scaleFactor != 1.0F && deltaFactor != 0F) {\n                mMatrix.postScale(deltaFactor + 1F, deltaFactor + 1F, mLastFocusX = mScaleGestureDetector.getFocusX(),\n                        mLastFocusY = mScaleGestureDetector.getFocusY());\n                setImageMatrix(mMatrix);\n            }\n            mPreScaleFactor = scaleFactor;\n            return false;\n        }\n\n        @Override\n        public boolean onScaleBegin(ScaleGestureDetector detector) {\n            // 注意返回true\n            return true;\n        }\n\n        @Override\n        public void onScaleEnd(ScaleGestureDetector detector) {\n        }\n    };\n\n    private float getScale() {\n        float[] values = new float[9];\n        mMatrix.getValues(values);\n        return values[Matrix.MSCALE_X];\n    }\n\n    private float[] getTransition() {\n        float[] values = new float[9];\n        mMatrix.getValues(values);\n        return new float[]{values[Matrix.MTRANS_X], values[Matrix.MTRANS_Y]};\n    }\n\n    /**\n     * 计算d的1/count次幂\n     *\n     * @param d\n     * @param count 开根的次数\n     * @return 相对值\n     */\n    private static float getRelativeValue(double d, double count) {\n        if (count == 0) {\n            return 1F;\n        }\n        count = 1 / count;\n        return (float) Math.pow(d, count);\n    }\n\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        super.onDraw(canvas);\n        if (mIsDragging) {\n            canvas.save();\n            drawLine(canvas);\n            canvas.restore();\n        }\n    }\n\n    // 绘制九宫线条\n    private void drawLine(Canvas canvas) {\n        // 开始点\n        float startX = 0;\n        float startY = 0;\n\n        // 结束点\n        float endX = 0;\n        float endY = 0;\n\n        RectF rectF = getMatrixRectF();\n\n        startX = rectF.left <= 0 ? 0 : rectF.left;\n        startY = rectF.top <= 0 ? 0 : rectF.top;\n\n        endX = rectF.right >= getWidth() ? getWidth() : rectF.right;\n        endY = rectF.bottom >= getHeight() ? getHeight() : rectF.bottom;\n\n        float lineWidth = 0;\n        float lineHeight = 0;\n\n        lineWidth = endX - startX;\n        lineHeight = endY - startY;\n\n        // LINE_ROW_NUMBER = 3 表示多少行\n        for (int i = 1; i < LINE_ROW_NUMBER; i++) {\n            canvas.drawLine(startX + 0, startY + lineHeight / LINE_ROW_NUMBER * i, endX, startY + lineHeight / LINE_ROW_NUMBER * i, mLinePaint);\n        }\n\n        // LINE_COLUMN_NUMBER = 3 表示多少列\n        for (int i = 1; i < LINE_COLUMN_NUMBER; i++) {\n            canvas.drawLine(startX + lineWidth / LINE_COLUMN_NUMBER * i, startY, startX + lineWidth / LINE_COLUMN_NUMBER * i, endY, mLinePaint);\n        }\n\n        mHandler.removeCallbacks(lineRunnable);\n        mHandler.postDelayed(lineRunnable, 400);\n    }\n\n\n    // 处理手指滑动\n    private GestureDetector.SimpleOnGestureListener mSimpleOnGestureListener = new GestureDetector.SimpleOnGestureListener() {\n\n        @Override\n        public boolean onDown(MotionEvent e) {\n            return true;\n        }\n\n        @Override\n        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {\n            if (e1.getPointerCount() == e2.getPointerCount() && e1.getPointerCount() == 1) {\n                mIsDragging = true;\n                // 获取图片矩阵\n                RectF rectF = getMatrixRectF();\n\n                float leftEdgeDistanceLeft = rectF.left;\n                float topEdgeDistanceTop = rectF.top;\n\n                float rightEdgeDistanceRight = leftEdgeDistanceLeft + rectF.right - rectF.left - getWidth();\n                float bottomEdgeDistanceBottom = topEdgeDistanceTop + rectF.bottom - rectF.top - getHeight();\n\n                // MAX_SCROLL_FACTOR = 3\n                int maxOffsetWidth = getWidth() / MAX_SCROLL_FACTOR;\n                int maxOffsetHeight = getHeight() / MAX_SCROLL_FACTOR;\n\n                // 图片左侧越界并且图片右侧未越界\n                if (leftEdgeDistanceLeft > 0 && rightEdgeDistanceRight > 0) {\n                    // distanceX < 0 表示继续向右滑动\n                    if (distanceX < 0) {\n                        if (leftEdgeDistanceLeft < maxOffsetWidth) {\n                            // DAMP_FACTOR = 9 系数越大阻尼越大  +1防止ratio为0\n                            int ratio = (int) (DAMP_FACTOR / maxOffsetWidth * leftEdgeDistanceLeft) + 1;\n                            distanceX /= ratio;\n                        } else {\n                            // 图片向右滑动超过了最大偏移量 图片则不平移\n                            distanceX = 0;\n                        }\n                    }\n                    // 向左滑动不做处理 默认取值distanceX\n                }\n                // 图片右侧越界并且图片左侧未越界 （同上处理）\n                else if (rightEdgeDistanceRight < 0 && leftEdgeDistanceLeft < 0) {\n                    // distanceX > 0 表示继续向左滑动\n                    if (distanceX > 0) {\n                        if (rightEdgeDistanceRight > -maxOffsetWidth) {\n                            int ratio = (int) (DAMP_FACTOR / maxOffsetWidth * -rightEdgeDistanceRight) + 1;\n                            distanceX /= ratio;\n                        } else {\n                            // 图片右侧距离控件右侧超过最大偏移量 图片则不平移\n                            distanceX = 0;\n                        }\n                    }\n                }\n                // 图片左侧越界并且图片右侧越界\n                else if (leftEdgeDistanceLeft > 0 && rightEdgeDistanceRight < 0) {\n                    // 控件宽度的一半\n                    int halfWidth = getWidth() / 2;\n                    // 获取图片中点x坐标\n                    float centerX = (rectF.right - rectF.left) / 2 + rectF.left;\n                    // 图片中点x坐标是否右侧偏移\n                    boolean rightOffsetCenterX = centerX >= halfWidth;\n                    // 右侧偏移并且向右滑动\n                    if (distanceX < 0 && rightOffsetCenterX) {\n                        // centerX - halfWidth 图片右侧偏移量\n                        int ratio = (int) (DAMP_FACTOR / maxOffsetWidth * (centerX - halfWidth)) + 1;\n                        distanceX /= ratio;\n                    }\n                    // 左侧偏移并且向左滑动\n                    else if (distanceX > 0 && !rightOffsetCenterX) {\n                        // halfWidth - centerX 左侧的偏移量\n                        int ratio = (int) (DAMP_FACTOR / maxOffsetWidth * (halfWidth - centerX)) + 1;\n                        distanceX /= ratio;\n                    }\n                }\n\n                // 上下越界 处理方式同左右处理方式一样 本可以提成一个方法但为了方便理解先这样了\n                // 图片上侧越界并且图片下侧未越界\n                if (topEdgeDistanceTop > 0 && bottomEdgeDistanceBottom > 0) {\n                    // distanceY < 0 表示图片继续向下滑动\n                    if (distanceY < 0) {\n                        if (topEdgeDistanceTop < maxOffsetHeight) {\n                            // 获取阻尼比例\n                            int ratio = (int) (DAMP_FACTOR / maxOffsetHeight * topEdgeDistanceTop) + 1;\n                            distanceY /= ratio;\n                        } else {\n                            // 向下滑动超过了最大偏移量 则图片不滑动\n                            distanceY = 0;\n                        }\n                    }\n                }\n                // 图片下侧越界并且图片上侧未越界\n                else if (bottomEdgeDistanceBottom < 0 && topEdgeDistanceTop < 0) {\n                    if (distanceY > 0) {\n                        if (bottomEdgeDistanceBottom > -maxOffsetHeight) {\n                            int ratio = (int) (DAMP_FACTOR / maxOffsetHeight * -bottomEdgeDistanceBottom) + 1;\n                            distanceY /= ratio;\n                        } else {\n                            // 向上滑动超过了最大偏移量 则图片不滑动\n                            distanceY = 0;\n                        }\n                    }\n                } else if (topEdgeDistanceTop > 0 && bottomEdgeDistanceBottom < 0) {\n                    int halfHeight = getHeight() / 2;\n                    // 获取图片中点y坐标\n                    float centerY = (rectF.bottom - rectF.top) / 2 + rectF.top;\n                    // 图片中点y坐标是否向下偏移\n                    boolean bottomOffsetCenterY = centerY >= halfHeight;\n                    // 向下偏移并且向下移动\n                    if (distanceY < 0 && bottomOffsetCenterY) {\n                        // centerY - halfHeight 图片偏移量\n                        int ratio = (int) (DAMP_FACTOR / maxOffsetHeight * (centerY - halfHeight)) + 1;\n                        distanceY /= ratio;\n                    } else if (distanceY > 0 && !bottomOffsetCenterY) { // 向上偏移并且向上移动\n                        int ratio = (int) (DAMP_FACTOR / maxOffsetHeight * (halfHeight - centerY)) + 1;\n                        distanceY /= ratio;\n                    }\n                }\n\n                mMatrix.postTranslate(-distanceX, -distanceY);\n                setImageMatrix(mMatrix);\n                return true;\n            }\n            return super.onScroll(e1, e2, distanceX, distanceY);\n        }\n    };\n\n    private Handler mHandler = new Handler() {\n        @Override\n        public void handleMessage(Message msg) {\n            super.handleMessage(msg);\n            if (msg != null) {\n                if (mCurrentScaleAnimCount < SCALE_ANIM_COUNT) {\n                    float obj = (float) msg.obj;\n                    mMatrix.postScale(obj, obj, mLastFocusX, mLastFocusY);\n                    setImageMatrix(mMatrix);\n                    mCurrentScaleAnimCount++;\n                    // what scale > mMaxScale 取0 不然取 1\n                    sendScaleMessage(obj, msg.what, SCALE_ANIM_COUNT);\n                } else if (mCurrentScaleAnimCount >= SCALE_ANIM_COUNT) {\n                    float[] values = new float[9];\n                    mMatrix.getValues(values);\n                    if (msg.what == ZOOM_OUT_ANIM_WHIT) {\n                        values[Matrix.MSCALE_X] = mMaxScale;\n                        values[Matrix.MSCALE_Y] = mMaxScale;\n                    } else if (msg.what == ZOOM_ANIM_WHIT) {\n                        values[Matrix.MSCALE_X] = mBaseScale;\n                        values[Matrix.MSCALE_Y] = mBaseScale;\n                    }\n                    mMatrix.setValues(values);\n                    setImageMatrix(mMatrix);\n\n                    // 边界检测\n                    boundCheck();\n                }\n            }\n        }\n    };\n\n    /**\n     * 发送消息\n     *\n     * @param relativeScale\n     * @param what\n     * @param delayMillis\n     */\n    private void sendScaleMessage(float relativeScale, int what, long delayMillis) {\n        Message mes = new Message();\n        mes.obj = relativeScale;\n        mes.what = what;\n        mHandler.sendMessageDelayed(mes, delayMillis);\n    }\n\n    public static int dip2px(Context context, float dpValue) {\n        float scale = context.getResources().getDisplayMetrics().density;\n        return (int) (dpValue * scale + 0.5f);\n    }\n\n    private Runnable lineRunnable = new Runnable() {\n        @Override\n        public void run() {\n            mIsDragging = false;\n            invalidate();\n        }\n    };\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/demo/mcropimageview/MainActivity.java",
    "content": "package com.demo.mcropimageview;\n\nimport android.support.v7.app.AppCompatActivity;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.TextView;\n\npublic class MainActivity extends AppCompatActivity {\n\n    MCropImageView mMCropImageView;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_main);\n\n        mMCropImageView = findViewById(R.id.crop_view);\n\n        findViewById(R.id.tv).setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n               //  mMCropImageView.drawAidLine();\n            }\n        });\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportHeight=\"108\"\n    android:viewportWidth=\"108\">\n    <path\n        android:fillColor=\"#26A69A\"\n        android:pathData=\"M0,0h108v108h-108z\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M9,0L9,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,0L19,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,0L29,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,0L39,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,0L49,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,0L59,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,0L69,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,0L79,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M89,0L89,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M99,0L99,108\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,9L108,9\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,19L108,19\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,29L108,29\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,39L108,39\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,49L108,49\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,59L108,59\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,69L108,69\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,79L108,79\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,89L108,89\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,99L108,99\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,29L89,29\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,39L89,39\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,49L89,49\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,59L89,59\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,69L89,69\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,79L89,79\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,19L29,89\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,19L39,89\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,19L49,89\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,19L59,89\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,19L69,89\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,19L79,89\"\n        android:strokeColor=\"#33FFFFFF\"\n        android:strokeWidth=\"0.8\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportHeight=\"108\"\n    android:viewportWidth=\"108\">\n    <path\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z\"\n        android:strokeColor=\"#00000000\"\n        android:strokeWidth=\"1\">\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n                android:endX=\"78.5885\"\n                android:endY=\"90.9159\"\n                android:startX=\"48.7653\"\n                android:startY=\"61.0927\"\n                android:type=\"linear\">\n                <item\n                    android:color=\"#44000000\"\n                    android:offset=\"0.0\" />\n                <item\n                    android:color=\"#00000000\"\n                    android:offset=\"1.0\" />\n            </gradient>\n        </aapt:attr>\n    </path>\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:fillType=\"nonZero\"\n        android:pathData=\"M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z\"\n        android:strokeColor=\"#00000000\"\n        android:strokeWidth=\"1\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<android.support.constraint.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".MainActivity\">\n\n    <com.demo.mcropimageview.MCropImageView\n        android:id=\"@+id/crop_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"#FF4081\"\n        android:src=\"@mipmap/ic_gril\" />\n\n    <TextView\n        android:id=\"@+id/tv\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"48dp\"\n        app:layout_constraintTop_toBottomOf=\"@+id/crop_view\" />\n\n</android.support.constraint.ConstraintLayout>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#3F51B5</color>\n    <color name=\"colorPrimaryDark\">#303F9F</color>\n    <color name=\"colorAccent\">#FF4081</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">MCropImageView</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "app/src/test/java/com/demo/mcropimageview/ExampleUnitTest.java",
    "content": "package com.demo.mcropimageview;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\npublic class ExampleUnitTest {\n    @Test\n    public void addition_isCorrect() {\n        assertEquals(4, 2 + 2);\n    }\n}"
  },
  {
    "path": "build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    \n    repositories {\n        google()\n        jcenter()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:3.1.3'\n        \n\n        // NOTE: Do not place your application dependencies here; they belong\n        // in the individual module build.gradle files\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        jcenter()\n    }\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Mon Feb 25 17:10:15 CST 2019\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-4.4-all.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx1536m\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=$(save \"$@\")\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\n# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong\nif [ \"$(uname)\" = \"Darwin\" ] && [ \"$HOME\" = \"$PWD\" ]; then\n  cd \"$(dirname \"$0\")\"\nfi\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto init\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto init\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:init\n@rem Get command-line arguments, handling Windows variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\n\n:win9xME_args\n@rem Slurp the command line arguments.\nset CMD_LINE_ARGS=\nset _SKIP=2\n\n:win9xME_args_slurp\nif \"x%~1\" == \"x\" goto execute\n\nset CMD_LINE_ARGS=%*\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "settings.gradle",
    "content": "include ':app'\n"
  }
]