├── res
├── values
│ ├── attrs.xml
│ └── strings.xml
├── drawable-hdpi
│ ├── test.png
│ └── ic_launcher.png
├── drawable-ldpi
│ └── ic_launcher.png
├── drawable-mdpi
│ └── ic_launcher.png
├── drawable-xhdpi
│ └── ic_launcher.png
├── menu
│ └── main.xml
└── layout
│ └── main.xml
├── local.properties
├── project.properties
├── ant.properties
├── src
└── me
│ └── isming
│ └── crop
│ ├── DemoActivity.java
│ ├── view
│ ├── CropImageLayout.java
│ ├── CropImageBorderView.java
│ ├── ZoomableImageView.java
│ └── CropZoomableImageView.java
│ ├── util
│ └── CLog.java
│ ├── Crop.java
│ ├── MonitoredActivity.java
│ ├── CropImageActivity.java
│ └── CropUtil.java
├── proguard-project.txt
├── .gitignore
├── AndroidManifest.xml
└── README.md
/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/res/drawable-hdpi/test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sangmingming/AndroidImageCrop/HEAD/res/drawable-hdpi/test.png
--------------------------------------------------------------------------------
/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sangmingming/AndroidImageCrop/HEAD/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/drawable-ldpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sangmingming/AndroidImageCrop/HEAD/res/drawable-ldpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sangmingming/AndroidImageCrop/HEAD/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sangmingming/AndroidImageCrop/HEAD/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | ImageCropView
4 |
5 |
--------------------------------------------------------------------------------
/res/menu/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/local.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must *NOT* be checked into Version Control Systems,
5 | # as it contains information specific to your local configuration.
6 |
7 | # location of the SDK. This is only used by Ant
8 | # For customization when using a Version Control System, please read the
9 | # header note.
10 | sdk.dir=/Users/sam/developer/android-sdk-macosx
11 |
--------------------------------------------------------------------------------
/project.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must be checked in Version Control Systems.
5 | #
6 | # To customize properties used by the Ant build system edit
7 | # "ant.properties", and override values to adapt the script to your
8 | # project structure.
9 | #
10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
12 |
13 | # Project target.
14 | target=android-19
15 |
--------------------------------------------------------------------------------
/ant.properties:
--------------------------------------------------------------------------------
1 | # This file is used to override default values used by the Ant build system.
2 | #
3 | # This file must be checked into Version Control Systems, as it is
4 | # integral to the build system of your project.
5 |
6 | # This file is only used by the Ant script.
7 |
8 | # You can use this to override default values such as
9 | # 'source.dir' for the location of your java source folder and
10 | # 'out.dir' for the location of your output folder.
11 |
12 | # You can also use it define how the release builds are signed by declaring
13 | # the following properties:
14 | # 'key.store' for the location of your keystore and
15 | # 'key.alias' for the name of the key to use.
16 | # The password will be asked during the build when you use the 'release' target.
17 |
18 |
--------------------------------------------------------------------------------
/src/me/isming/crop/DemoActivity.java:
--------------------------------------------------------------------------------
1 | package me.isming.crop;
2 |
3 | import android.app.Activity;
4 | import android.net.Uri;
5 | import android.os.Bundle;
6 | import android.os.Environment;
7 |
8 | import java.io.File;
9 |
10 | /**
11 | * Created by sam on 14-10-17.
12 | *
13 | * this is a demo
14 | */
15 | public class DemoActivity extends Activity {
16 | @Override
17 | public void onCreate(Bundle savedInstanceState) {
18 | super.onCreate(savedInstanceState);
19 | new Crop(Uri.fromFile(new File(Environment.getExternalStorageDirectory() + "/pic/jjjj.jpg")))
20 | .output(Uri.fromFile(new File(Environment.getExternalStorageDirectory() + "/pic/first.jpg")))
21 | .withWidth(640)
22 | .start(this);
23 | }
24 | }
--------------------------------------------------------------------------------
/proguard-project.txt:
--------------------------------------------------------------------------------
1 | # To enable ProGuard in your project, edit project.properties
2 | # to define the proguard.config property as described in that file.
3 | #
4 | # Add project specific ProGuard rules here.
5 | # By default, the flags in this file are appended to flags specified
6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt
7 | # You can edit the include path and order by changing the ProGuard
8 | # include property in project.properties.
9 | #
10 | # For more details, see
11 | # http://developer.android.com/guide/developing/tools/proguard.html
12 |
13 | # Add any project specific keep options here:
14 |
15 | # If your project uses WebView with JS, uncomment the following
16 | # and specify the fully qualified class name to the JavaScript interface
17 | # class:
18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
19 | # public *;
20 | #}
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .gitignore support plugin (hsz.mobi)
2 | ### JetBrains template
3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
4 |
5 | ## Directory-based project format
6 | .idea/
7 | # if you remove the above rule, at least ignore user-specific stuff:
8 | # .idea/workspace.xml
9 | # .idea/tasks.xml
10 | # and these sensitive or high-churn files:
11 | # .idea/dataSources.ids
12 | # .idea/dataSources.xml
13 | # .idea/sqlDataSources.xml
14 | # .idea/dynamic.xml
15 |
16 | ## File-based project format
17 | *.ipr
18 | *.iml
19 | *.iws
20 |
21 | ## Additional for IntelliJ
22 | out/
23 |
24 | # generated by mpeltonen/sbt-idea plugin
25 | .idea_modules/
26 |
27 | # generated by JIRA plugin
28 | atlassian-ide-plugin.xml
29 |
30 | # generated by Crashlytics plugin (for Android Studio and Intellij)
31 | com_crashlytics_export_strings.xml
32 |
33 | bin/
34 | gen/
35 |
--------------------------------------------------------------------------------
/res/layout/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/me/isming/crop/view/CropImageLayout.java:
--------------------------------------------------------------------------------
1 | package me.isming.crop.view;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.graphics.BitmapFactory;
6 | import android.util.AttributeSet;
7 | import android.view.ViewGroup;
8 | import android.widget.RelativeLayout;
9 | import me.isming.crop.R;
10 |
11 | /**
12 | * Created by sam on 14-10-17.
13 | */
14 | public class CropImageLayout extends RelativeLayout {
15 |
16 | private CropZoomableImageView mZoomImageView;
17 | private CropImageBorderView mClipImageView;
18 |
19 | public final static int MAX_WIDTH = 2048;
20 |
21 | private int mHorizontalPadding = 20;
22 |
23 | public CropImageLayout(Context context) {
24 | this(context, null);
25 | }
26 |
27 | public CropImageLayout(Context context, AttributeSet attrs) {
28 | this(context, attrs, 0);
29 | }
30 |
31 | public CropImageLayout(Context context, AttributeSet attrs, int defStyle) {
32 | super(context, attrs, defStyle);
33 |
34 | mZoomImageView = new CropZoomableImageView(context);
35 | mClipImageView = new CropImageBorderView(context);
36 |
37 | ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
38 | ViewGroup.LayoutParams.MATCH_PARENT);
39 | addView(mZoomImageView, params);
40 | addView(mClipImageView, params);
41 |
42 | mZoomImageView.setImageResource(R.drawable.test);
43 | }
44 |
45 | public Bitmap clip() {
46 | return mZoomImageView.clip();
47 | }
48 |
49 | public void setImageBitmap(Bitmap bitmap) {
50 | mZoomImageView.setImageBitmap(bitmap);
51 | mZoomImageView.reLayout();
52 | mZoomImageView.invalidate();
53 | }
54 |
55 | public void setImagePath(String filePath) {
56 | Bitmap b = BitmapFactory.decodeFile(filePath);
57 | setImageBitmap(b);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Image Crop for Android
2 |
3 | this is a image crop for android.
4 | that can get square image.
5 |
6 |
7 | # How to use it
8 |
9 | 1. use crop activity
10 | ```java
11 | new Crop(Uri.fromFile(
12 | new File(Environment.getExternalStorageDirectory() + "/pic/jjjj.jpg"))) //the picture want to crop
13 | .output(Uri.fromFile(new File(Environment.getExternalStorageDirectory() + "/pic/first.jpg"))) //the file to save crop
14 | .withWidth(640) //the max width want to save
15 | .start(this);
16 | ```
17 |
18 | need add the content in you AndroidManifest.xml file:
19 |
20 | ```xml
21 |
22 | ```
23 |
24 | can see demo in the DemoActivity.
25 |
26 | 2.use crop view
27 |
28 | use in xml layout
29 |
30 | ```xml
31 |
35 | ```
36 |
37 | or in java class:
38 |
39 | ```java
40 | new CropImageLayout(context);
41 | ```
42 |
43 | # Author
44 |
45 | if has other question, can contact me by my weibo,blog,email:
46 |
47 | blog: [http://blog.isming.me](http://blog.isming.me)
48 |
49 | Weibo: [@码农明明桑](http://weibo.com/mingmingsang)
50 |
51 | E-Mail: linming1007@gmail.com
52 |
53 |
54 | 写英文好痛苦啊
55 | 感觉再也不会爱了
56 |
57 | ## License
58 | Copyright 2014 isming.me
59 |
60 | Licensed under the Apache License, Version 2.0 (the "License");
61 | you may not use this file except in compliance with the License.
62 | You may obtain a copy of the License at
63 |
64 | http://www.apache.org/licenses/LICENSE-2.0
65 |
66 | Unless required by applicable law or agreed to in writing, software
67 | distributed under the License is distributed on an "AS IS" BASIS,
68 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
69 | See the License for the specific language governing permissions and
70 | limitations under the License.
--------------------------------------------------------------------------------
/src/me/isming/crop/view/CropImageBorderView.java:
--------------------------------------------------------------------------------
1 | package me.isming.crop.view;
2 |
3 | import android.content.Context;
4 | import android.graphics.Canvas;
5 | import android.graphics.Color;
6 | import android.graphics.Paint;
7 | import android.util.AttributeSet;
8 | import android.util.TypedValue;
9 | import android.view.View;
10 |
11 | /**
12 | * Created by sam on 14-10-16.
13 | */
14 | public class CropImageBorderView extends View {
15 |
16 | private int mHorizontalPadding = 0;
17 | private int mVerticalPadding;
18 | private int mWidth;
19 | private int mBorderColor = Color.parseColor("#FFFFFF");
20 | private int mBorderWidth = 1;
21 | private Paint mPaint;
22 |
23 | public CropImageBorderView(Context context) {
24 | this(context, null);
25 | }
26 |
27 | public CropImageBorderView(Context context, AttributeSet attrs) {
28 | this(context, attrs, 0);
29 | }
30 |
31 | public CropImageBorderView(Context context, AttributeSet attrs, int defStyleAttr) {
32 | super(context, attrs, defStyleAttr);
33 |
34 | mHorizontalPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
35 | mHorizontalPadding, getResources().getDisplayMetrics());
36 | mBorderWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
37 | mBorderWidth, getResources().getDisplayMetrics());
38 | mPaint = new Paint();
39 | mPaint.setAntiAlias(true);
40 | }
41 |
42 | @Override
43 | protected void onDraw(Canvas canvas) {
44 | super.onDraw(canvas);
45 | mWidth = getWidth() - 2 * mHorizontalPadding;
46 |
47 | mVerticalPadding = (getHeight() - mWidth) / 2;
48 | mPaint.setColor(Color.parseColor("#AA000000"));
49 | mPaint.setStyle(Paint.Style.FILL);
50 |
51 | canvas.drawRect(0, 0, mHorizontalPadding, getHeight(), mPaint);
52 | canvas.drawRect(getWidth() - mHorizontalPadding, 0, getWidth(), getHeight(), mPaint);
53 | canvas.drawRect(mHorizontalPadding, 0, getWidth() - mHorizontalPadding, mVerticalPadding, mPaint);
54 | canvas.drawRect(mHorizontalPadding, getHeight() - mVerticalPadding, getWidth() - mHorizontalPadding,
55 | getHeight(), mPaint);
56 | mPaint.setColor(mBorderColor);
57 | mPaint.setStrokeWidth(mBorderWidth);
58 | mPaint.setStyle(Paint.Style.STROKE);
59 | canvas.drawRect(mHorizontalPadding, mVerticalPadding, getWidth() - mHorizontalPadding,
60 | getHeight() - mVerticalPadding, mPaint);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/me/isming/crop/util/CLog.java:
--------------------------------------------------------------------------------
1 | package me.isming.crop.util;
2 |
3 | import android.util.Log;
4 |
5 | /**
6 | * Created by sam on 14-8-4.
7 | */
8 | public class CLog {
9 | public static final String LOG_TAG = CLog.class.getSimpleName();
10 |
11 | public static final boolean DEBUG = true;
12 |
13 | private static final String LOG_PREFIX = "CDLOG_";
14 |
15 | private static final int LOG_PREFIX_LENGTH = LOG_PREFIX.length();
16 |
17 | private static final int MAX_LOG_TAG_LENGTH = 23;
18 |
19 | public static String makeLogTag(String str) {
20 | if (str.length() > MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH) {
21 | return LOG_PREFIX + str.substring(0, MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH - 1);
22 | }
23 | return LOG_PREFIX + str;
24 | }
25 |
26 | /**
27 | * WARNING: Don't use this when obfuscating class names with Proguard!
28 | */
29 | public static String makeLogTag(Class> cls) {
30 | return makeLogTag(cls.getSimpleName());
31 | }
32 |
33 | public static void i(String tag, String msg, Throwable throwable) {
34 | if (DEBUG) {
35 | Log.i(tag, msg, throwable);
36 | }
37 | }
38 |
39 | public static void i(String tag, Object... objects) {
40 | StringBuilder builder = new StringBuilder();
41 | for (Object obj : objects) {
42 | builder.append(obj);
43 | }
44 | i(tag, builder.toString());
45 | }
46 |
47 | public static void i(String tag, String msg) {
48 | i(tag, msg, null);
49 | }
50 |
51 | public static void i(String msg) {
52 | i(LOG_TAG, msg, null);
53 | }
54 |
55 | public static void d(String tag, String msg, Throwable throwable) {
56 | if (DEBUG) {
57 | Log.d(tag, msg, throwable);
58 | }
59 | }
60 |
61 | public static void d(String tag, String msg) {
62 | d(tag, msg, null);
63 | }
64 |
65 | public static void d(String msg) {
66 | d(LOG_TAG, msg, null);
67 | }
68 |
69 | public static void w(String tag, String msg, Throwable throwable) {
70 | if (DEBUG) {
71 | Log.w(tag, msg, throwable);
72 | }
73 | }
74 |
75 | public static void w(String tag, String msg) {
76 | w(tag, msg, null);
77 | }
78 |
79 | public static void w(String msg) {
80 | w(LOG_TAG, msg, null);
81 | }
82 |
83 | public static void e(String tag, String msg, Throwable throwable) {
84 | if (DEBUG) {
85 | Log.e(tag, msg, throwable);
86 | }
87 | }
88 |
89 | public static void e(String tag, String msg) {
90 | e(tag, msg, null);
91 | }
92 |
93 | public static void e(String msg) {
94 | e(LOG_TAG, msg, null);
95 | }
96 |
97 | private CLog() {
98 | }
99 |
100 | }
101 |
--------------------------------------------------------------------------------
/src/me/isming/crop/Crop.java:
--------------------------------------------------------------------------------
1 | package me.isming.crop;
2 |
3 | import android.annotation.TargetApi;
4 | import android.app.Activity;
5 | import android.app.Fragment;
6 | import android.content.Context;
7 | import android.content.Intent;
8 | import android.net.Uri;
9 | import android.os.Build;
10 | import android.provider.MediaStore;
11 |
12 | /**
13 | * Builder for crop Intents and utils for handling result
14 | * Created by sam on 14-10-16.
15 | */
16 | public class Crop {
17 |
18 | public static final int REQUEST_CROP = 6709;
19 |
20 | static interface Extra {
21 | String MAX_WIDTH = "max_width";
22 | String ERROR = "error";
23 | }
24 |
25 | private Intent cropIntent;
26 |
27 | /**
28 | * Create a crop Intent builder with source image
29 | *
30 | * @param source Source image URI
31 | */
32 | public Crop(Uri source) {
33 | cropIntent = new Intent();
34 | cropIntent.setData(source);
35 | }
36 |
37 | /**
38 | * Set output URI where the cropped image will be saved
39 | *
40 | * @param output Output image URI
41 | */
42 | public Crop output(Uri output) {
43 | cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, output);
44 | return this;
45 | }
46 |
47 |
48 | /**
49 | * Set maximum crop size
50 | *
51 | * @param width Max width
52 | */
53 | public Crop withWidth(int width) {
54 | cropIntent.putExtra(Extra.MAX_WIDTH, width);
55 | return this;
56 | }
57 |
58 | /**
59 | * Send the crop Intent!
60 | *
61 | * @param activity Activity that will receive result
62 | */
63 | public void start(Activity activity) {
64 | activity.startActivityForResult(getIntent(activity), REQUEST_CROP);
65 | }
66 |
67 | /**
68 | * Send the crop Intent!
69 | *
70 | * @param context Context
71 | * @param fragment Fragment that will receive result
72 | */
73 | @TargetApi(Build.VERSION_CODES.HONEYCOMB)
74 | public void start(Context context, Fragment fragment) {
75 | fragment.startActivityForResult(getIntent(context), REQUEST_CROP);
76 | }
77 |
78 | private Intent getIntent(Context context) {
79 | cropIntent.setClass(context, CropImageActivity.class);
80 | return cropIntent;
81 | }
82 |
83 | /**
84 | * Retrieve URI for cropped image, as set in the Intent builder
85 | *
86 | * @param result Output Image URI
87 | */
88 | public static Uri getOutput(Intent result) {
89 | return result.getParcelableExtra(MediaStore.EXTRA_OUTPUT);
90 | }
91 |
92 | /**
93 | * Retrieve error that caused crop to fail
94 | *
95 | * @param result Result Intent
96 | * @return Throwable handled in CropImageActivity
97 | */
98 | public static Throwable getError(Intent result) {
99 | return (Throwable) result.getSerializableExtra(Extra.ERROR);
100 | }
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/src/me/isming/crop/MonitoredActivity.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2009 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package me.isming.crop;
18 |
19 | import android.app.Activity;
20 | import android.os.Bundle;
21 |
22 | import java.util.ArrayList;
23 |
24 | /*
25 | * Modified from original in AOSP.
26 | */
27 | abstract class MonitoredActivity extends Activity {
28 |
29 | private final ArrayList listeners = new ArrayList();
30 |
31 | public static interface LifeCycleListener {
32 | public void onActivityCreated(MonitoredActivity activity);
33 | public void onActivityDestroyed(MonitoredActivity activity);
34 | public void onActivityStarted(MonitoredActivity activity);
35 | public void onActivityStopped(MonitoredActivity activity);
36 | }
37 |
38 | public static class LifeCycleAdapter implements LifeCycleListener {
39 | public void onActivityCreated(MonitoredActivity activity) {}
40 | public void onActivityDestroyed(MonitoredActivity activity) {}
41 | public void onActivityStarted(MonitoredActivity activity) {}
42 | public void onActivityStopped(MonitoredActivity activity) {}
43 | }
44 |
45 | public void addLifeCycleListener(LifeCycleListener listener) {
46 | if (listeners.contains(listener)) return;
47 | listeners.add(listener);
48 | }
49 |
50 | public void removeLifeCycleListener(LifeCycleListener listener) {
51 | listeners.remove(listener);
52 | }
53 |
54 | @Override
55 | protected void onCreate(Bundle savedInstanceState) {
56 | super.onCreate(savedInstanceState);
57 | for (LifeCycleListener listener : listeners) {
58 | listener.onActivityCreated(this);
59 | }
60 | }
61 |
62 | @Override
63 | protected void onDestroy() {
64 | super.onDestroy();
65 | for (LifeCycleListener listener : listeners) {
66 | listener.onActivityDestroyed(this);
67 | }
68 | }
69 |
70 | @Override
71 | protected void onStart() {
72 | super.onStart();
73 | for (LifeCycleListener listener : listeners) {
74 | listener.onActivityStarted(this);
75 | }
76 | }
77 |
78 | @Override
79 | protected void onStop() {
80 | super.onStop();
81 | for (LifeCycleListener listener : listeners) {
82 | listener.onActivityStopped(this);
83 | }
84 | }
85 |
86 | }
87 |
--------------------------------------------------------------------------------
/src/me/isming/crop/CropImageActivity.java:
--------------------------------------------------------------------------------
1 | package me.isming.crop;
2 |
3 | import android.content.Intent;
4 | import android.graphics.Bitmap;
5 | import android.graphics.BitmapFactory;
6 | import android.net.Uri;
7 | import android.os.Bundle;
8 | import android.provider.MediaStore;
9 | import android.util.DisplayMetrics;
10 | import android.view.Menu;
11 | import android.view.MenuItem;
12 | import me.isming.crop.util.CLog;
13 | import me.isming.crop.view.CropImageLayout;
14 |
15 | import java.io.IOException;
16 | import java.io.InputStream;
17 | import java.io.OutputStream;
18 |
19 | public class CropImageActivity extends MonitoredActivity {
20 |
21 | private CropImageLayout mImageView;
22 | private Uri mSaveUri;
23 | private int mMaxWidth;
24 | /**
25 | * Called when the activity is first created.
26 | */
27 | @Override
28 | public void onCreate(Bundle savedInstanceState) {
29 | super.onCreate(savedInstanceState);
30 | setContentView(R.layout.main);
31 | mImageView = (CropImageLayout) findViewById(R.id.clip);
32 | setupFromIntent();
33 | }
34 |
35 |
36 | private void setupFromIntent() {
37 | Intent intent = getIntent();
38 | Bundle extras = intent.getExtras();
39 |
40 | if (extras != null) {
41 | mMaxWidth = extras.getInt(Crop.Extra.MAX_WIDTH);
42 | mSaveUri = extras.getParcelable(MediaStore.EXTRA_OUTPUT);
43 | }
44 |
45 | Uri data = getIntent().getData();
46 | if (data == null) {
47 | finish();
48 | }
49 | InputStream is = null;
50 | try {
51 | int sampleSize = calculateBitmapSampleSize(data);
52 | BitmapFactory.Options options = new BitmapFactory.Options();
53 | options.inSampleSize = sampleSize;
54 | is = getContentResolver().openInputStream(data);
55 | Bitmap b = BitmapFactory.decodeStream(is, null, options);
56 | mImageView.setImageBitmap(b);
57 | } catch (Exception e) {
58 | e.printStackTrace();
59 | }finally {
60 | CropUtil.closeSilently(is);
61 | }
62 | }
63 |
64 |
65 | private int calculateBitmapSampleSize(Uri bitmapUri) throws IOException {
66 | InputStream is = null;
67 | BitmapFactory.Options options = new BitmapFactory.Options();
68 | options.inJustDecodeBounds = true;
69 | try {
70 | is = getContentResolver().openInputStream(bitmapUri);
71 | BitmapFactory.decodeStream(is, null, options); // Just get image size
72 | } finally {
73 | CropUtil.closeSilently(is);
74 | }
75 |
76 | int maxSize = getMaxImageSize();
77 | int sampleSize = 1;
78 | while (options.outHeight / sampleSize > maxSize || options.outWidth / sampleSize > maxSize) {
79 | sampleSize = sampleSize << 1;
80 | }
81 |
82 | CLog.e("SampleSize" + sampleSize);
83 | return sampleSize;
84 | }
85 |
86 | private int getMaxImageSize() {
87 | int textureLimit = getScreenHeight();
88 | if (textureLimit == 0) {
89 | return CropImageLayout.MAX_WIDTH;
90 | } else {
91 | return Math.min(textureLimit, CropImageLayout.MAX_WIDTH);
92 | }
93 | }
94 |
95 | private int getScreenHeight() {
96 | DisplayMetrics dm = new DisplayMetrics();
97 | getWindowManager().getDefaultDisplay().getMetrics(dm);
98 | return dm.heightPixels;
99 | }
100 |
101 | @Override
102 | public boolean onOptionsItemSelected(MenuItem item) {
103 | switch (item.getItemId()) {
104 | case R.id.clip:
105 | saveOutput();
106 | break;
107 | }
108 | return super.onOptionsItemSelected(item);
109 | }
110 |
111 | @Override
112 | public boolean onCreateOptionsMenu(Menu menu) {
113 | getMenuInflater().inflate(R.menu.main, menu);
114 | return super.onCreateOptionsMenu(menu);
115 | }
116 |
117 |
118 | private void saveOutput() {
119 | if (mSaveUri != null) {
120 | OutputStream outputStream = null;
121 | try {
122 | outputStream = getContentResolver().openOutputStream(mSaveUri);
123 | Bitmap b = mImageView.clip();
124 |
125 | if (outputStream != null && b != null) {
126 | if (mMaxWidth >0 && b.getWidth() > mMaxWidth) {
127 | b = Bitmap.createScaledBitmap(b, mMaxWidth, mMaxWidth, true);
128 | }
129 | b.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
130 | }
131 | } catch (IOException e) {
132 | CLog.e("Cannot open file: " + mSaveUri);
133 | } finally {
134 | CropUtil.closeSilently(outputStream);
135 | }
136 |
137 |
138 | }
139 | }
140 |
141 |
142 | }
143 |
--------------------------------------------------------------------------------
/src/me/isming/crop/CropUtil.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2009 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package me.isming.crop;
18 |
19 | import android.app.ProgressDialog;
20 | import android.content.ContentResolver;
21 | import android.database.Cursor;
22 | import android.media.ExifInterface;
23 | import android.net.Uri;
24 | import android.os.Handler;
25 | import android.provider.MediaStore;
26 | import android.text.TextUtils;
27 |
28 | import me.isming.crop.util.CLog;
29 |
30 | import java.io.Closeable;
31 | import java.io.File;
32 | import java.io.IOException;
33 |
34 | /*
35 | * Modified from original in AOSP.
36 | */
37 | class CropUtil {
38 |
39 | private static final String SCHEME_FILE = "file";
40 | private static final String SCHEME_CONTENT = "content";
41 |
42 | public static void closeSilently(Closeable c) {
43 | if (c == null) return;
44 | try {
45 | c.close();
46 | } catch (Throwable t) {
47 | // Do nothing
48 | }
49 | }
50 |
51 | public static int getExifRotation(File imageFile) {
52 | if (imageFile == null) return 0;
53 | try {
54 | ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
55 | // We only recognize a subset of orientation tag values
56 | switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)) {
57 | case ExifInterface.ORIENTATION_ROTATE_90:
58 | return 90;
59 | case ExifInterface.ORIENTATION_ROTATE_180:
60 | return 180;
61 | case ExifInterface.ORIENTATION_ROTATE_270:
62 | return 270;
63 | default:
64 | return ExifInterface.ORIENTATION_UNDEFINED;
65 | }
66 | } catch (IOException e) {
67 | CLog.e("Error getting Exif data" + e.getMessage());
68 | return 0;
69 | }
70 | }
71 |
72 | public static boolean copyExifRotation(File sourceFile, File destFile) {
73 | if (sourceFile == null || destFile == null) return false;
74 | try {
75 | ExifInterface exifSource = new ExifInterface(sourceFile.getAbsolutePath());
76 | ExifInterface exifDest = new ExifInterface(destFile.getAbsolutePath());
77 | exifDest.setAttribute(ExifInterface.TAG_ORIENTATION, exifSource.getAttribute(ExifInterface.TAG_ORIENTATION));
78 | exifDest.saveAttributes();
79 | return true;
80 | } catch (IOException e) {
81 | CLog.e("Error copying Exif data" + e.getMessage());
82 | return false;
83 | }
84 | }
85 |
86 | public static File getFromMediaUri(ContentResolver resolver, Uri uri) {
87 | if (uri == null) return null;
88 |
89 | if (SCHEME_FILE.equals(uri.getScheme())) {
90 | return new File(uri.getPath());
91 | } else if (SCHEME_CONTENT.equals(uri.getScheme())) {
92 | final String[] filePathColumn = { MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME };
93 | Cursor cursor = null;
94 | try {
95 | cursor = resolver.query(uri, filePathColumn, null, null, null);
96 | if (cursor != null && cursor.moveToFirst()) {
97 | final int columnIndex = (uri.toString().startsWith("content://com.google.android.gallery3d")) ?
98 | cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) :
99 | cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
100 | // Picasa image on newer devices with Honeycomb and up
101 | if (columnIndex != -1) {
102 | String filePath = cursor.getString(columnIndex);
103 | if (!TextUtils.isEmpty(filePath)) {
104 | return new File(filePath);
105 | }
106 | }
107 | }
108 | } catch (SecurityException ignored) {
109 | // Nothing we can do
110 | } finally {
111 | if (cursor != null) cursor.close();
112 | }
113 | }
114 | return null;
115 | }
116 |
117 | public static void startBackgroundJob(MonitoredActivity activity,
118 | String title, String message, Runnable job, Handler handler) {
119 | // Make the progress dialog uncancelable, so that we can gurantee
120 | // the thread will be done before the activity getting destroyed
121 | ProgressDialog dialog = ProgressDialog.show(
122 | activity, title, message, true, false);
123 | new Thread(new BackgroundJob(activity, job, dialog, handler)).start();
124 | }
125 |
126 | private static class BackgroundJob extends MonitoredActivity.LifeCycleAdapter implements Runnable {
127 |
128 | private final MonitoredActivity mActivity;
129 | private final ProgressDialog mDialog;
130 | private final Runnable mJob;
131 | private final Handler mHandler;
132 | private final Runnable mCleanupRunner = new Runnable() {
133 | public void run() {
134 | mActivity.removeLifeCycleListener(BackgroundJob.this);
135 | if (mDialog.getWindow() != null) mDialog.dismiss();
136 | }
137 | };
138 |
139 | public BackgroundJob(MonitoredActivity activity, Runnable job,
140 | ProgressDialog dialog, Handler handler) {
141 | mActivity = activity;
142 | mDialog = dialog;
143 | mJob = job;
144 | mActivity.addLifeCycleListener(this);
145 | mHandler = handler;
146 | }
147 |
148 | public void run() {
149 | try {
150 | mJob.run();
151 | } finally {
152 | mHandler.post(mCleanupRunner);
153 | }
154 | }
155 |
156 | @Override
157 | public void onActivityDestroyed(MonitoredActivity activity) {
158 | // We get here only when the onDestroyed being called before
159 | // the mCleanupRunner. So, run it now and remove it from the queue
160 | mCleanupRunner.run();
161 | mHandler.removeCallbacks(mCleanupRunner);
162 | }
163 |
164 | @Override
165 | public void onActivityStopped(MonitoredActivity activity) {
166 | mDialog.hide();
167 | }
168 |
169 | @Override
170 | public void onActivityStarted(MonitoredActivity activity) {
171 | mDialog.show();
172 | }
173 | }
174 |
175 | }
176 |
--------------------------------------------------------------------------------
/src/me/isming/crop/view/ZoomableImageView.java:
--------------------------------------------------------------------------------
1 | package me.isming.crop.view;
2 |
3 | import android.content.Context;
4 | import android.graphics.Matrix;
5 | import android.graphics.RectF;
6 | import android.graphics.drawable.Drawable;
7 | import android.util.AttributeSet;
8 | import android.view.*;
9 | import android.widget.ImageView;
10 | import me.isming.crop.util.CLog;
11 |
12 | /**
13 | * Created by sam on 14-10-16.
14 | */
15 | public class ZoomableImageView extends ImageView implements ScaleGestureDetector.OnScaleGestureListener,
16 | View.OnTouchListener, ViewTreeObserver.OnGlobalLayoutListener{
17 |
18 | private static final float SCALE_MAX = 4.0f;
19 | private static final float SCALE_MID = 2.0f;
20 |
21 | /**
22 | * 初始缩放比例。如果图片宽度或高度大于屏幕,此值将小于0
23 | */
24 | private float initScale = 1.0f;
25 |
26 | private final float[] matrixValues = new float[9]; //用于存放矩阵的9个值
27 |
28 | private boolean once = true;
29 |
30 | private ScaleGestureDetector mScaleGestureDetector = null; //缩放手势的检测
31 |
32 | private final Matrix mScaleMatrix = new Matrix();
33 |
34 | private GestureDetector mGestureDetector;
35 | private boolean isAutoScale;
36 |
37 | private int mTouchSlop;
38 | private boolean isCheckTopAndBottom = true;
39 | private boolean isCheckLeftAndRight = true;
40 |
41 | public ZoomableImageView(Context context) {
42 | this(context, null);
43 | }
44 |
45 | public ZoomableImageView(Context context, AttributeSet attrs) {
46 | this(context, attrs, 0);
47 | }
48 |
49 | public ZoomableImageView(Context context, AttributeSet attrs, int defStyle) {
50 | super(context, attrs, defStyle);
51 | setScaleType(ScaleType.MATRIX);
52 | mScaleGestureDetector = new ScaleGestureDetector(context, this);
53 | setOnTouchListener(this);
54 | final ViewConfiguration configuration = ViewConfiguration.get(context);
55 | // 获得可以认为是滚动的距离
56 | mTouchSlop = configuration.getScaledTouchSlop();
57 | //mTouchSlop = 4;
58 |
59 | mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
60 | @Override
61 | public boolean onDoubleTap(MotionEvent e) {
62 | CLog.i("Double tap");
63 | if (isAutoScale == true) {
64 | CLog.i("Double tap is true");
65 | return true;
66 | }
67 | float x = e.getX();
68 | float y = e.getY();
69 | if (getScale() < SCALE_MID) {
70 | //postDelayed() scale_MID
71 | CLog.i("Double tap mid");
72 | ZoomableImageView.this.postDelayed(new AutoScaleRunnable(SCALE_MID, x, y), 16);
73 | isAutoScale = true;
74 | } else if (getScale() >= SCALE_MID && getScale() <= SCALE_MAX) {
75 | // scale_max
76 | CLog.i("Double tap is max");
77 | ZoomableImageView.this.postDelayed(new AutoScaleRunnable(SCALE_MAX, x, y), 16);
78 | isAutoScale = true;
79 | } else {
80 | //initScale
81 | CLog.i("Double tap is initscale");
82 | ZoomableImageView.this.postDelayed(new AutoScaleRunnable(initScale, x, y), 16);
83 | isAutoScale = true;
84 | }
85 | return true;
86 | }
87 | });
88 |
89 |
90 | }
91 |
92 | @Override
93 | public void onGlobalLayout() {
94 | if (once) {
95 | Drawable d = getDrawable();
96 | if (d == null) {
97 | return;
98 | }
99 | CLog.e("drawable.intrinsicWidth:" + d.getIntrinsicWidth() +
100 | ",drawable.intrinsicHeight:" + d.getIntrinsicHeight());
101 | int width = getWidth();
102 | int height = getHeight();
103 |
104 | int dw = d.getIntrinsicWidth();
105 | int dh = d.getIntrinsicHeight();
106 |
107 | float scale = 1.0f;
108 |
109 | if (dw > width && dh <= height) {
110 | scale = width * 1.0f / dw;
111 | }
112 |
113 | if (dh > height && dw <= width) {
114 | scale = height * 1.0f /dh;
115 | }
116 |
117 | if (dw > width && dh > height) {
118 | scale = Math.min(dw * 1.0f / width, dh * 1.0f / height);
119 | }
120 |
121 | initScale = scale;
122 |
123 | mScaleMatrix.postTranslate((width - dw) / 2, (height - dh) /2);
124 | mScaleMatrix.postScale(scale, scale, getWidth() / 2, getHeight() / 2);
125 | setImageMatrix(mScaleMatrix);
126 | once = false;
127 |
128 |
129 |
130 | }
131 | }
132 |
133 | @Override
134 | public boolean onScale(ScaleGestureDetector detector) {
135 | float scale = getScale();
136 | float scaleFactor = detector.getScaleFactor();
137 |
138 | if (getDrawable() == null) {
139 | return true;
140 | }
141 |
142 | if ((scale < SCALE_MAX && scaleFactor > 1.0f) || (scale > initScale && scaleFactor < 1.0f)) {
143 | if (scaleFactor * scale < initScale) {
144 | scaleFactor = initScale / scale;
145 | }
146 |
147 | if (scaleFactor * scale > SCALE_MAX) {
148 | scaleFactor = SCALE_MAX / scale;
149 | }
150 |
151 | //mScaleMatrix.postScale(scaleFactor, scaleFactor, getWidth() / 2, getHeight() / 2);
152 | mScaleMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
153 | checkBorderAndCenterWhenScale();
154 | setImageMatrix(mScaleMatrix);
155 | }
156 | return true;
157 | }
158 |
159 | @Override
160 | public boolean onScaleBegin(ScaleGestureDetector detector) {
161 | return true;
162 | }
163 |
164 | @Override
165 | public void onScaleEnd(ScaleGestureDetector detector) {
166 |
167 | }
168 |
169 | private float mLastX, mLastY;
170 | private int mLastPointerCount;
171 | private boolean mIsCanDrag;
172 |
173 | @Override
174 | public boolean onTouch(View v, MotionEvent event) {
175 | if (mGestureDetector.onTouchEvent(event)) {
176 | return true;
177 | }
178 | mScaleGestureDetector.onTouchEvent(event);
179 | float x = 0, y = 0;
180 |
181 | final int pointerCount = event.getPointerCount();
182 |
183 | for (int i = 0; i < pointerCount; i++) {
184 | x += event.getX(i);
185 | y += event.getY(i);
186 |
187 | }
188 |
189 | x = x / pointerCount;
190 | y = y / pointerCount;
191 |
192 | if (pointerCount != mLastPointerCount) {
193 | mIsCanDrag = false;
194 | mLastX = x;
195 | mLastY = y;
196 | }
197 | mLastPointerCount = pointerCount;
198 | switch (event.getAction()) {
199 | case MotionEvent.ACTION_MOVE:
200 |
201 | float dx = x - mLastX;
202 | float dy = y - mLastY;
203 |
204 | if (!mIsCanDrag) {
205 | CLog.i("action move" + pointerCount + "show can drag");
206 | mIsCanDrag = isCanDrag(dx, dy);
207 | }
208 |
209 |
210 |
211 | if (mIsCanDrag) {
212 | CLog.i("can drag action move");
213 | RectF rectF = getMatrixRectF();
214 | if (getDrawable() != null) {
215 | isCheckLeftAndRight = isCheckTopAndBottom = true;
216 | //如果宽度小于屏幕宽度,则禁止左右移动
217 | if (rectF.width() < getWidth()) {
218 | dx = 0;
219 | isCheckLeftAndRight = false;
220 | }
221 | if (rectF.height() < getHeight()) {
222 | dy = 0;
223 | isCheckTopAndBottom = false;
224 | }
225 | mScaleMatrix.postTranslate(dx, dy);
226 | checkMatrixBounds();
227 | setImageMatrix(mScaleMatrix);
228 | }
229 | }
230 | mLastY = y;
231 | mLastX = x;
232 | break;
233 | case MotionEvent.ACTION_UP:
234 | case MotionEvent.ACTION_CANCEL:
235 | mLastPointerCount = 0;
236 | break;
237 |
238 | }
239 |
240 | return true;
241 | }
242 |
243 | @Override
244 | protected void onDetachedFromWindow() {
245 | super.onDetachedFromWindow();
246 | CLog.i("zoom image detached from window");
247 | getViewTreeObserver().removeGlobalOnLayoutListener(this);
248 | }
249 |
250 | @Override
251 | protected void onAttachedToWindow() {
252 | super.onAttachedToWindow();
253 | CLog.i("zoom image attached to window");
254 | getViewTreeObserver().addOnGlobalLayoutListener(this);
255 | }
256 |
257 | public final float getScale() {
258 | mScaleMatrix.getValues(matrixValues);
259 | return matrixValues[Matrix.MSCALE_X];
260 | }
261 |
262 | /**
263 | * 在缩放时,进行图片显示范围的控制
264 | */
265 | private void checkBorderAndCenterWhenScale() {
266 | RectF rect = getMatrixRectF();
267 | float deltaX = 0;
268 | float deltaY = 0;
269 |
270 | int width = getWidth();
271 | int height = getHeight();
272 |
273 | //如果宽或搞大于屏幕则控制范围
274 | if (rect.width() >= width) {
275 | if (rect.left > 0) {
276 | deltaX = -rect.left;
277 | }
278 | if (rect.right < width) {
279 | deltaX = width - rect.right;
280 | }
281 |
282 | }
283 |
284 | if (rect.height() >= height) {
285 | if (rect.top > 0) {
286 | deltaY = -rect.top;
287 | }
288 | if (rect.bottom < height) {
289 | deltaY = height - rect.bottom;
290 | }
291 | }
292 |
293 | //如果宽或高小于屏幕,则让其居中
294 | if (rect.width() < width) {
295 | deltaX = width * 0.5f - rect.right + 0.5f * rect.width();
296 | }
297 |
298 | if (rect.height() < height) {
299 | deltaY = height * 0.5f - rect.bottom + 0.5f * rect.height();
300 | }
301 |
302 | mScaleMatrix.postTranslate(deltaX, deltaY);
303 |
304 |
305 | }
306 |
307 | private RectF getMatrixRectF() {
308 | Matrix matrix = mScaleMatrix;
309 | RectF rect = new RectF();
310 | Drawable d = getDrawable();
311 | if (d != null) {
312 | rect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
313 | matrix.mapRect(rect);
314 | }
315 | return rect;
316 | }
317 |
318 | private void checkMatrixBounds() {
319 | RectF rect = getMatrixRectF();
320 | float deltaX = 0, deltaY = 0;
321 | final float viewWidth = getWidth();
322 | final float viewHeight = getHeight();
323 |
324 | if (rect.top > 0 && isCheckTopAndBottom) {
325 | deltaY = -rect.top;
326 | }
327 | if (rect.bottom < viewHeight && isCheckTopAndBottom) {
328 | deltaY = viewHeight - rect.bottom;
329 | }
330 | if (rect.left > 0 && isCheckLeftAndRight) {
331 | deltaX = -rect.left;
332 | }
333 | if (rect.right < viewWidth && isCheckLeftAndRight) {
334 | deltaX = viewWidth - rect.right;
335 | }
336 | mScaleMatrix.postTranslate(deltaX, deltaY);
337 | }
338 |
339 | private boolean isCanDrag(float dx, float dy) {
340 | CLog.i("x:" + Math.sqrt((dx * dx) + (dy * dy)) + "y:" + mTouchSlop);
341 | return Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop;
342 | }
343 |
344 | private class AutoScaleRunnable implements Runnable {
345 | static final float BIGGER = 1.07f;
346 | static final float SMALLER = 0.93f;
347 | private float mTargetScale;
348 | private float tmpScale;
349 |
350 | private float x;
351 | private float y;
352 |
353 | public AutoScaleRunnable(float targetScale, float x, float y) {
354 | mTargetScale = targetScale;
355 | this.x = x;
356 | this.y = y;
357 | if (getScale() < mTargetScale) {
358 | tmpScale = BIGGER;
359 | } else {
360 | tmpScale = SMALLER;
361 | }
362 |
363 |
364 | }
365 |
366 | @Override
367 | public void run() {
368 | mScaleMatrix.postScale(tmpScale, tmpScale, x, y);
369 | checkBorderAndCenterWhenScale();
370 | setImageMatrix(mScaleMatrix);
371 |
372 | final float currentScale = getScale();
373 |
374 | if(((tmpScale > 1f) && (currentScale < mTargetScale)) || ((tmpScale < 1f) && (mTargetScale < currentScale))) {
375 | ZoomableImageView.this.postDelayed(this, 16);
376 | } else {
377 | final float deltaScale = mTargetScale / currentScale;
378 | mScaleMatrix.postScale(deltaScale, deltaScale, x, y);
379 | checkBorderAndCenterWhenScale();
380 | setImageMatrix(mScaleMatrix);
381 | isAutoScale = false;
382 | }
383 | }
384 | }
385 | }
386 |
--------------------------------------------------------------------------------
/src/me/isming/crop/view/CropZoomableImageView.java:
--------------------------------------------------------------------------------
1 | package me.isming.crop.view;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.graphics.Canvas;
6 | import android.graphics.Matrix;
7 | import android.graphics.RectF;
8 | import android.graphics.drawable.Drawable;
9 | import android.util.AttributeSet;
10 | import android.util.TypedValue;
11 | import android.view.*;
12 | import android.widget.ImageView;
13 | import me.isming.crop.util.CLog;
14 |
15 | /**
16 | * Created by sam on 14-10-16.
17 | */
18 | public class CropZoomableImageView extends ImageView implements ScaleGestureDetector.OnScaleGestureListener,
19 | View.OnTouchListener, ViewTreeObserver.OnGlobalLayoutListener{
20 |
21 | private float SCALE_MAX = 4.0f;
22 | private float SCALE_MID = 2.0f;
23 |
24 | /**
25 | * 初始缩放比例。如果图片宽度或高度大于屏幕,此值将小于0
26 | */
27 | private float initScale = 1.0f;
28 |
29 | private final float[] matrixValues = new float[9]; //用于存放矩阵的9个值
30 |
31 | private boolean once = true;
32 |
33 | private ScaleGestureDetector mScaleGestureDetector = null; //缩放手势的检测
34 |
35 | private final Matrix mScaleMatrix = new Matrix();
36 |
37 | private GestureDetector mGestureDetector;
38 | private boolean isAutoScale;
39 |
40 | private int mTouchSlop;
41 | private boolean isCheckTopAndBottom = true;
42 | private boolean isCheckLeftAndRight = true;
43 |
44 | private int mHorizontalPadding = 0;
45 | private int mVerticalPadding;
46 |
47 | public CropZoomableImageView(Context context) {
48 | this(context, null);
49 | }
50 |
51 | public CropZoomableImageView(Context context, AttributeSet attrs) {
52 | this(context, attrs, 0);
53 | }
54 |
55 | public CropZoomableImageView(Context context, AttributeSet attrs, int defStyle) {
56 | super(context, attrs, defStyle);
57 | setScaleType(ScaleType.MATRIX);
58 | mScaleGestureDetector = new ScaleGestureDetector(context, this);
59 | setOnTouchListener(this);
60 | final ViewConfiguration configuration = ViewConfiguration.get(context);
61 | // 获得可以认为是滚动的距离
62 | mTouchSlop = configuration.getScaledTouchSlop();
63 | //mTouchSlop = 4;
64 |
65 | mHorizontalPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
66 | mHorizontalPadding, getResources().getDisplayMetrics());
67 |
68 | mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
69 | @Override
70 | public boolean onDoubleTap(MotionEvent e) {
71 | if (isAutoScale == true) {
72 | return true;
73 | }
74 | float x = e.getX();
75 | float y = e.getY();
76 | if (getScale() < SCALE_MID) {
77 | CropZoomableImageView.this.postDelayed(new AutoScaleRunnable(SCALE_MID, x, y), 16);
78 | isAutoScale = true;
79 | } else if (getScale() >= SCALE_MID && getScale() <= SCALE_MAX) {
80 | CropZoomableImageView.this.postDelayed(new AutoScaleRunnable(SCALE_MAX, x, y), 16);
81 | isAutoScale = true;
82 | } else {
83 | CropZoomableImageView.this.postDelayed(new AutoScaleRunnable(initScale, x, y), 16);
84 | isAutoScale = true;
85 | }
86 | return true;
87 | }
88 | });
89 |
90 |
91 | }
92 |
93 | public void reLayout() {
94 | once = true;
95 | }
96 |
97 | @Override
98 | public void onGlobalLayout() {
99 | if (once) {
100 | Drawable d = getDrawable();
101 | if (d == null) {
102 | return;
103 | }
104 | CLog.e("drawable.intrinsicWidth:" + d.getIntrinsicWidth() +
105 | ",drawable.intrinsicHeight:" + d.getIntrinsicHeight());
106 | int width = getWidth();
107 | int height = getHeight();
108 |
109 | int dw = d.getIntrinsicWidth();
110 | int dh = d.getIntrinsicHeight();
111 |
112 | float scale = 1.0f;
113 |
114 | scale = Math.max(width * 1.0f / dw, width * 1.0f /dh);
115 |
116 | initScale = scale;
117 | SCALE_MAX = initScale * 4;
118 | SCALE_MID = initScale *2;
119 | mVerticalPadding = (getHeight() - (getWidth() - 2 * mHorizontalPadding)) / 2;
120 |
121 | mScaleMatrix.postTranslate((width - dw) / 2, (height - dh) /2);
122 | mScaleMatrix.postScale(scale, scale, getWidth() / 2, getHeight() / 2);
123 | setImageMatrix(mScaleMatrix);
124 | once = false;
125 |
126 | }
127 | }
128 |
129 | /**
130 | * 裁剪图片,返回裁剪后的bitmap对象
131 | * @return
132 | */
133 | public Bitmap clip() {
134 | Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
135 | Canvas canvas = new Canvas(bitmap);
136 | draw(canvas);
137 | return Bitmap.createBitmap(bitmap, mHorizontalPadding, mVerticalPadding, getWidth() - 2 * mHorizontalPadding,
138 | getHeight() - 2 * mVerticalPadding);
139 | }
140 |
141 | @Override
142 | public boolean onScale(ScaleGestureDetector detector) {
143 | float scale = getScale();
144 | float scaleFactor = detector.getScaleFactor();
145 |
146 | if (getDrawable() == null) {
147 | return true;
148 | }
149 |
150 | if ((scale < SCALE_MAX && scaleFactor > 1.0f) || (scale > initScale && scaleFactor < 1.0f)) {
151 | if (scaleFactor * scale < initScale) {
152 | scaleFactor = initScale / scale;
153 | }
154 |
155 | if (scaleFactor * scale > SCALE_MAX) {
156 | scaleFactor = SCALE_MAX / scale;
157 | }
158 |
159 | mScaleMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
160 | checkBorderAndCenterWhenScale();
161 | setImageMatrix(mScaleMatrix);
162 | }
163 | return true;
164 | }
165 |
166 | @Override
167 | public boolean onScaleBegin(ScaleGestureDetector detector) {
168 | return true;
169 | }
170 |
171 | @Override
172 | public void onScaleEnd(ScaleGestureDetector detector) {
173 |
174 | }
175 |
176 | private float mLastX, mLastY;
177 | private int mLastPointerCount;
178 | private boolean mIsCanDrag;
179 |
180 | @Override
181 | public boolean onTouch(View v, MotionEvent event) {
182 | if (mGestureDetector.onTouchEvent(event)) {
183 | return true;
184 | }
185 | mScaleGestureDetector.onTouchEvent(event);
186 | float x = 0, y = 0;
187 |
188 | final int pointerCount = event.getPointerCount();
189 |
190 | for (int i = 0; i < pointerCount; i++) {
191 | x += event.getX(i);
192 | y += event.getY(i);
193 |
194 | }
195 |
196 | x = x / pointerCount;
197 | y = y / pointerCount;
198 |
199 | if (pointerCount != mLastPointerCount) {
200 | mIsCanDrag = false;
201 | mLastX = x;
202 | mLastY = y;
203 | }
204 | mLastPointerCount = pointerCount;
205 | switch (event.getAction()) {
206 | case MotionEvent.ACTION_MOVE:
207 |
208 | float dx = x - mLastX;
209 | float dy = y - mLastY;
210 |
211 | if (!mIsCanDrag) {
212 | CLog.i("action move" + pointerCount + "show can drag");
213 | mIsCanDrag = isCanDrag(dx, dy);
214 | }
215 |
216 |
217 |
218 | if (mIsCanDrag) {
219 | CLog.i("can drag action move");
220 | RectF rectF = getMatrixRectF();
221 | if (getDrawable() != null) {
222 | isCheckLeftAndRight = isCheckTopAndBottom = true;
223 | //如果宽度小于屏幕宽度,则禁止左右移动
224 | if (rectF.width() < getWidth() - 2 * mHorizontalPadding) {
225 | dx = 0;
226 | isCheckLeftAndRight = false;
227 | }
228 | if (rectF.height() < getHeight() - 2 * mVerticalPadding) {
229 | dy = 0;
230 | isCheckTopAndBottom = false;
231 | }
232 | mScaleMatrix.postTranslate(dx, dy);
233 | checkMatrixBounds();
234 | setImageMatrix(mScaleMatrix);
235 | }
236 | }
237 | mLastY = y;
238 | mLastX = x;
239 | break;
240 | case MotionEvent.ACTION_UP:
241 | case MotionEvent.ACTION_CANCEL:
242 | mLastPointerCount = 0;
243 | break;
244 |
245 | }
246 |
247 | return true;
248 | }
249 |
250 | @Override
251 | protected void onDetachedFromWindow() {
252 | super.onDetachedFromWindow();
253 | getViewTreeObserver().removeGlobalOnLayoutListener(this);
254 | }
255 |
256 | @Override
257 | protected void onAttachedToWindow() {
258 | super.onAttachedToWindow();
259 | getViewTreeObserver().addOnGlobalLayoutListener(this);
260 | }
261 |
262 | public final float getScale() {
263 | mScaleMatrix.getValues(matrixValues);
264 | return matrixValues[Matrix.MSCALE_X];
265 | }
266 |
267 | /**
268 | * 在缩放时,进行图片显示范围的控制
269 | */
270 | private void checkBorderAndCenterWhenScale() {
271 | RectF rect = getMatrixRectF();
272 | float deltaX = 0;
273 | float deltaY = 0;
274 |
275 | int width = getWidth();
276 | int height = getHeight();
277 |
278 | //如果宽或搞大于屏幕则控制范围
279 | if (rect.width() >= width - 2 * mHorizontalPadding) {
280 | if (rect.left > mHorizontalPadding) {
281 | deltaX = -rect.left + mHorizontalPadding;
282 | }
283 | if (rect.right < width - mHorizontalPadding) {
284 | deltaX = width - rect.right - mHorizontalPadding;
285 | }
286 |
287 | }
288 |
289 | if (rect.height() >= height - 2 * mVerticalPadding) {
290 | if (rect.top > 0) {
291 | deltaY = -rect.top + mVerticalPadding;
292 | }
293 | if (rect.bottom < height - mVerticalPadding) {
294 | deltaY = height - rect.bottom - mVerticalPadding;
295 | }
296 | }
297 |
298 | //如果宽或高小于屏幕,则让其居中
299 | if (rect.width() < width) {
300 | deltaX = width * 0.5f - rect.right + 0.5f * rect.width();
301 | }
302 |
303 | if (rect.height() < height) {
304 | deltaY = height * 0.5f - rect.bottom + 0.5f * rect.height();
305 | }
306 |
307 | mScaleMatrix.postTranslate(deltaX, deltaY);
308 |
309 |
310 | }
311 |
312 | private RectF getMatrixRectF() {
313 | Matrix matrix = mScaleMatrix;
314 | RectF rect = new RectF();
315 | Drawable d = getDrawable();
316 | if (d != null) {
317 | rect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
318 | matrix.mapRect(rect);
319 | }
320 | return rect;
321 | }
322 |
323 | private void checkMatrixBounds() {
324 | RectF rect = getMatrixRectF();
325 | float deltaX = 0, deltaY = 0;
326 | final float viewWidth = getWidth();
327 | final float viewHeight = getHeight();
328 |
329 | if (rect.top > mVerticalPadding && isCheckTopAndBottom) {
330 | deltaY = -rect.top + mVerticalPadding;
331 | }
332 | if (rect.bottom < viewHeight - mVerticalPadding && isCheckTopAndBottom) {
333 | deltaY = viewHeight - rect.bottom - mVerticalPadding;
334 | }
335 | if (rect.left > mHorizontalPadding && isCheckLeftAndRight) {
336 | deltaX = -rect.left + mHorizontalPadding;
337 | }
338 | if (rect.right < viewWidth - mHorizontalPadding && isCheckLeftAndRight) {
339 | deltaX = viewWidth - rect.right - mHorizontalPadding;
340 | }
341 | mScaleMatrix.postTranslate(deltaX, deltaY);
342 | }
343 |
344 | private boolean isCanDrag(float dx, float dy) {
345 | CLog.i("x:" + Math.sqrt((dx * dx) + (dy * dy)) + "y:" + mTouchSlop);
346 | return Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop;
347 | }
348 |
349 | private class AutoScaleRunnable implements Runnable {
350 | static final float BIGGER = 1.07f;
351 | static final float SMALLER = 0.93f;
352 | private float mTargetScale;
353 | private float tmpScale;
354 |
355 | private float x;
356 | private float y;
357 |
358 | public AutoScaleRunnable(float targetScale, float x, float y) {
359 | mTargetScale = targetScale;
360 | this.x = x;
361 | this.y = y;
362 | if (getScale() < mTargetScale) {
363 | tmpScale = BIGGER;
364 | } else {
365 | tmpScale = SMALLER;
366 | }
367 | }
368 |
369 | @Override
370 | public void run() {
371 | mScaleMatrix.postScale(tmpScale, tmpScale, x, y);
372 | checkBorderAndCenterWhenScale();
373 | setImageMatrix(mScaleMatrix);
374 |
375 | final float currentScale = getScale();
376 |
377 | if(((tmpScale > 1f) && (currentScale < mTargetScale)) || ((tmpScale < 1f) && (mTargetScale < currentScale))) {
378 | CropZoomableImageView.this.postDelayed(this, 16);
379 | } else {
380 | final float deltaScale = mTargetScale / currentScale;
381 | mScaleMatrix.postScale(deltaScale, deltaScale, x, y);
382 | checkBorderAndCenterWhenScale();
383 | setImageMatrix(mScaleMatrix);
384 | isAutoScale = false;
385 | }
386 | }
387 | }
388 | }
389 |
--------------------------------------------------------------------------------