├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── mjj │ │ └── selectphotodemo │ │ ├── Application.java │ │ ├── FollowWeChatPhotoActivity.java │ │ ├── PhotoPickerActivity.java │ │ ├── PreviewPhotoActivity.java │ │ ├── adapters │ │ ├── FolderAdapter.java │ │ └── PhotoAdapter.java │ │ ├── beans │ │ ├── ImageItem.java │ │ ├── Photo.java │ │ └── PhotoFolder.java │ │ ├── io │ │ └── DiskLruCache.java │ │ ├── utils │ │ ├── Bimp.java │ │ ├── BitmapUtils.java │ │ ├── ImageLoader.java │ │ ├── MPermissionUtils │ │ ├── OtherUtils.java │ │ └── PhotoUtils.java │ │ ├── widgets │ │ └── SquareImageView.java │ │ └── zoom │ │ ├── Compat.java │ │ ├── IPhotoView.java │ │ ├── PhotoView.java │ │ ├── PhotoViewAttacher.java │ │ ├── SDK16.java │ │ ├── ScrollerProxy.java │ │ ├── VersionedGestureDetector.java │ │ └── ViewPagerFixed.java │ └── res │ ├── anim │ ├── anim_entry_from_bottom.xml │ └── anim_leave_from_bottom.xml │ ├── color │ ├── default_text_color.xml │ └── floder_name_color_selector.xml │ ├── drawable-xhdpi │ ├── btn_back.png │ ├── btn_selected.png │ ├── btn_unselected.png │ ├── ic_camera.png │ ├── ic_delete_white.png │ ├── ic_dir.png │ ├── ic_dir_choose.png │ ├── ic_launcher.png │ ├── ic_photo_loading.png │ ├── icon_addpic_focused.png │ ├── text_indicator_normal.png │ └── text_indicator_pressed.png │ ├── drawable │ ├── action_btn.xml │ ├── bg_camera_selector.xml │ ├── btn_select_selector.xml │ ├── layout_selector.xml │ ├── scrollbar_vertical_thumb.xml │ └── text_indicator_selector.xml │ ├── layout │ ├── activity_follow_wechat_photo.xml │ ├── activity_photo_picker.xml │ ├── fload_list_layout_stub.xml │ ├── folderlist_layout.xml │ ├── item_camera_layout.xml │ ├── item_folder_layout.xml │ ├── item_gridview.xml │ ├── item_image.xml │ ├── item_photo_layout.xml │ ├── plugin_camera_gallery.xml │ ├── plugin_camera_image_file.xml │ └── tabbar_layout.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 1.8 51 | 52 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.0" 6 | defaultConfig { 7 | applicationId "com.example.mjj.selectphotodemo" 8 | minSdkVersion 15 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(include: ['*.jar'], dir: 'libs') 24 | compile 'com.android.support:appcompat-v7:25.0.1' 25 | } 26 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\AndroidStudioSDK/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/Application.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo; 2 | 3 | import android.content.Context; 4 | 5 | /** 6 | * Description : 7 | */ 8 | public class Application extends android.app.Application { 9 | 10 | private static Context context; 11 | 12 | @Override 13 | public void onCreate() { 14 | super.onCreate(); 15 | context = this.getApplicationContext(); 16 | } 17 | 18 | public static Context getContext() { 19 | return context; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/FollowWeChatPhotoActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.graphics.Bitmap; 6 | import android.graphics.BitmapFactory; 7 | import android.graphics.Color; 8 | import android.graphics.drawable.ColorDrawable; 9 | import android.os.Bundle; 10 | import android.os.Handler; 11 | import android.os.Message; 12 | import android.support.annotation.Nullable; 13 | import android.support.v7.app.AppCompatActivity; 14 | import android.view.LayoutInflater; 15 | import android.view.View; 16 | import android.view.ViewGroup; 17 | import android.widget.AdapterView; 18 | import android.widget.BaseAdapter; 19 | import android.widget.GridView; 20 | import android.widget.ImageView; 21 | 22 | import com.example.mjj.selectphotodemo.beans.ImageItem; 23 | import com.example.mjj.selectphotodemo.utils.Bimp; 24 | import com.example.mjj.selectphotodemo.utils.BitmapUtils; 25 | 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | 29 | /** 30 | * Description:仿微信朋友圈图片选择功能 31 | *

32 | * Created by Mjj on 2016/12/2. 33 | */ 34 | 35 | public class FollowWeChatPhotoActivity extends AppCompatActivity { 36 | 37 | private static final int PICK_PHOTO = 1; 38 | private List mResults = new ArrayList<>(); 39 | 40 | public static Bitmap bimap; 41 | private GridView noScrollgridview; 42 | private GridAdapter adapter; 43 | 44 | @Override 45 | protected void onCreate(@Nullable Bundle savedInstanceState) { 46 | super.onCreate(savedInstanceState); 47 | 48 | bimap = BitmapFactory.decodeResource(getResources(), R.drawable.icon_addpic_focused); 49 | 50 | mResults.add(bimap); 51 | initView(); 52 | } 53 | 54 | private void initView() { 55 | setContentView(R.layout.activity_follow_wechat_photo); 56 | 57 | noScrollgridview = (GridView) findViewById(R.id.noScrollgridview); 58 | noScrollgridview.setSelector(new ColorDrawable(Color.TRANSPARENT)); 59 | adapter = new GridAdapter(this); 60 | adapter.update(); 61 | noScrollgridview.setAdapter(adapter); 62 | noScrollgridview.setOnItemClickListener(new AdapterView.OnItemClickListener() { 63 | 64 | @Override 65 | public void onItemClick(AdapterView adapterView, View view, int i, long l) { 66 | if (i == mResults.size() - 1 || i == Bimp.tempSelectBitmap.size()) { 67 | Intent intent = new Intent(FollowWeChatPhotoActivity.this, PhotoPickerActivity.class); 68 | intent.putExtra(PhotoPickerActivity.EXTRA_SHOW_CAMERA, true); 69 | intent.putExtra(PhotoPickerActivity.EXTRA_SELECT_MODE, PhotoPickerActivity.MODE_SINGLE); 70 | intent.putExtra(PhotoPickerActivity.EXTRA_MAX_MUN, PhotoPickerActivity.DEFAULT_NUM); 71 | // 总共选择的图片数量 72 | intent.putExtra(PhotoPickerActivity.TOTAL_MAX_MUN, Bimp.tempSelectBitmap.size()); 73 | startActivityForResult(intent, PICK_PHOTO); 74 | } else { 75 | Intent intent = new Intent(FollowWeChatPhotoActivity.this, 76 | PreviewPhotoActivity.class); 77 | intent.putExtra("position", "1"); 78 | intent.putExtra("ID", i); 79 | startActivity(intent); 80 | } 81 | } 82 | }); 83 | } 84 | 85 | @Override 86 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 87 | super.onActivityResult(requestCode, resultCode, data); 88 | if (requestCode == PICK_PHOTO) { 89 | if (resultCode == RESULT_OK) { 90 | ArrayList result = data.getStringArrayListExtra(PhotoPickerActivity.KEY_RESULT); 91 | showResult(result); 92 | } 93 | } 94 | } 95 | 96 | private void showResult(ArrayList paths) { 97 | if (mResults == null) { 98 | mResults = new ArrayList<>(); 99 | } 100 | if (paths.size() != 0) { 101 | mResults.remove(mResults.size() - 1); 102 | } 103 | for (int i = 0; i < paths.size(); i++) { 104 | // 压缩图片 105 | Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFd(paths.get(i), 400, 500); 106 | // 针对小图也可以不压缩 107 | // Bitmap bitmap = BitmapFactory.decodeFile(paths.get(i)); 108 | mResults.add(bitmap); 109 | 110 | ImageItem takePhoto = new ImageItem(); 111 | takePhoto.setBitmap(bitmap); 112 | Bimp.tempSelectBitmap.add(takePhoto); 113 | } 114 | mResults.add(BitmapFactory.decodeResource(getResources(), R.drawable.icon_addpic_focused)); 115 | adapter.notifyDataSetChanged(); 116 | } 117 | 118 | /** 119 | * 适配器 120 | */ 121 | public class GridAdapter extends BaseAdapter { 122 | 123 | private LayoutInflater inflater; 124 | 125 | public GridAdapter(Context context) { 126 | inflater = LayoutInflater.from(context); 127 | } 128 | 129 | public void update() { 130 | loading(); 131 | } 132 | 133 | public int getCount() { 134 | if (Bimp.tempSelectBitmap.size() == 9) { 135 | return 9; 136 | } 137 | return (Bimp.tempSelectBitmap.size() + 1); 138 | } 139 | 140 | public Object getItem(int arg0) { 141 | return mResults.get(arg0); 142 | } 143 | 144 | public long getItemId(int arg0) { 145 | return arg0; 146 | } 147 | 148 | public View getView(int position, View convertView, ViewGroup parent) { 149 | ViewHolder holder = null; 150 | if (convertView == null) { 151 | convertView = inflater.inflate(R.layout.item_gridview, null); 152 | holder = new ViewHolder(); 153 | holder.image = (ImageView) convertView 154 | .findViewById(R.id.imageView1); 155 | convertView.setTag(holder); 156 | } else { 157 | holder = (ViewHolder) convertView.getTag(); 158 | } 159 | 160 | if (position == Bimp.tempSelectBitmap.size()) { 161 | holder.image.setImageBitmap(BitmapFactory.decodeResource( 162 | getResources(), R.drawable.icon_addpic_focused)); 163 | if (position == 9) { 164 | holder.image.setVisibility(View.GONE); 165 | } 166 | } else { 167 | holder.image.setImageBitmap(Bimp.tempSelectBitmap.get(position).getBitmap()); 168 | } 169 | return convertView; 170 | } 171 | 172 | public class ViewHolder { 173 | public ImageView image; 174 | } 175 | 176 | Handler handler = new Handler() { 177 | public void handleMessage(Message msg) { 178 | switch (msg.what) { 179 | case 1: 180 | adapter.notifyDataSetChanged(); 181 | break; 182 | } 183 | super.handleMessage(msg); 184 | } 185 | }; 186 | 187 | public void loading() { 188 | new Thread(new Runnable() { 189 | public void run() { 190 | while (true) { 191 | if (Bimp.max == Bimp.tempSelectBitmap.size()) { 192 | Message message = new Message(); 193 | message.what = 1; 194 | handler.sendMessage(message); 195 | break; 196 | } else { 197 | Bimp.max += 1; 198 | Message message = new Message(); 199 | message.what = 1; 200 | handler.sendMessage(message); 201 | } 202 | } 203 | } 204 | }).start(); 205 | } 206 | } 207 | 208 | protected void onRestart() { 209 | adapter.update(); 210 | super.onRestart(); 211 | } 212 | 213 | } 214 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/PhotoPickerActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo; 2 | 3 | import android.Manifest; 4 | import android.animation.AnimatorSet; 5 | import android.animation.ObjectAnimator; 6 | import android.annotation.TargetApi; 7 | import android.app.Activity; 8 | import android.app.ProgressDialog; 9 | import android.content.Intent; 10 | import android.content.pm.PackageManager; 11 | import android.net.Uri; 12 | import android.os.AsyncTask; 13 | import android.os.Build; 14 | import android.os.Bundle; 15 | import android.provider.MediaStore; 16 | import android.support.v4.app.ActivityCompat; 17 | import android.support.v4.content.ContextCompat; 18 | import android.support.v7.app.AppCompatActivity; 19 | import android.util.TypedValue; 20 | import android.view.MotionEvent; 21 | import android.view.View; 22 | import android.view.ViewStub; 23 | import android.view.animation.LinearInterpolator; 24 | import android.widget.AdapterView; 25 | import android.widget.Button; 26 | import android.widget.GridView; 27 | import android.widget.ListView; 28 | import android.widget.TextView; 29 | import android.widget.Toast; 30 | 31 | import com.example.mjj.selectphotodemo.adapters.FolderAdapter; 32 | import com.example.mjj.selectphotodemo.adapters.PhotoAdapter; 33 | import com.example.mjj.selectphotodemo.beans.Photo; 34 | import com.example.mjj.selectphotodemo.beans.PhotoFolder; 35 | import com.example.mjj.selectphotodemo.utils.MPermissionUtils; 36 | import com.example.mjj.selectphotodemo.utils.OtherUtils; 37 | import com.example.mjj.selectphotodemo.utils.PhotoUtils; 38 | 39 | import java.io.File; 40 | import java.util.ArrayList; 41 | import java.util.List; 42 | import java.util.Map; 43 | import java.util.Set; 44 | 45 | /** 46 | * Description:仿微信图片选择界面 47 | *

48 | * Created by Mjj on 2016/12/2. 49 | */ 50 | public class PhotoPickerActivity extends AppCompatActivity implements PhotoAdapter.PhotoClickCallBack { 51 | 52 | public final static String KEY_RESULT = "picker_result"; 53 | public final static int REQUEST_CAMERA = 1; 54 | 55 | /** 56 | * 是否显示相机 57 | */ 58 | public final static String EXTRA_SHOW_CAMERA = "is_show_camera"; 59 | /** 60 | * 照片选择模式 61 | */ 62 | public final static String EXTRA_SELECT_MODE = "select_mode"; 63 | /** 64 | * 最大选择数量 65 | */ 66 | public final static String EXTRA_MAX_MUN = "max_num"; 67 | /** 68 | * 总共选择的图片数量 69 | */ 70 | public final static String TOTAL_MAX_MUN = "total_num"; 71 | /** 72 | * 单选 73 | */ 74 | public final static int MODE_SINGLE = 0; 75 | /** 76 | * 多选 77 | */ 78 | public final static int MODE_MULTI = 1; 79 | /** 80 | * 默认最大选择数量 81 | */ 82 | public final static int DEFAULT_NUM = 9; 83 | 84 | private final static String ALL_PHOTO = "所有图片"; 85 | /** 86 | * 是否显示相机,默认不显示 87 | */ 88 | private boolean mIsShowCamera = false; 89 | /** 90 | * 照片选择模式,默认是单选模式 91 | */ 92 | private int mSelectMode = 0; 93 | /** 94 | * 最大选择数量,仅多选模式有用 95 | */ 96 | private int mMaxNum; 97 | 98 | /** 99 | * 总共选择的图片数量 100 | */ 101 | private int mTotalNum; 102 | 103 | private GridView mGridView; 104 | private Map mFolderMap; 105 | private List mPhotoLists = new ArrayList<>(); 106 | private ArrayList mSelectList = new ArrayList<>(); 107 | private PhotoAdapter mPhotoAdapter; 108 | private ProgressDialog mProgressDialog; 109 | private ListView mFolderListView; 110 | 111 | private TextView mPhotoNumTV; 112 | private TextView mPhotoNameTV; 113 | private Button mCommitBtn; 114 | /** 115 | * 文件夹列表是否处于显示状态 116 | */ 117 | boolean mIsFolderViewShow = false; 118 | /** 119 | * 文件夹列表是否被初始化,确保只被初始化一次 120 | */ 121 | boolean mIsFolderViewInit = false; 122 | 123 | /** 124 | * 拍照时存储拍照结果的临时文件 125 | */ 126 | private File mTmpFile; 127 | 128 | @Override 129 | protected void onCreate(Bundle savedInstanceState) { 130 | super.onCreate(savedInstanceState); 131 | setContentView(R.layout.activity_photo_picker); 132 | initIntentParams(); 133 | initView(); 134 | if (!OtherUtils.isExternalStorageAvailable()) { 135 | Toast.makeText(this, "No SD card!", Toast.LENGTH_SHORT).show(); 136 | return; 137 | } 138 | 139 | // 检查权限 140 | // checkPerm(); 141 | MPermissionUtils.requestPermissionsResult(PhotoPickerActivity.this, 1, 142 | new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE, 143 | Manifest.permission.READ_EXTERNAL_STORAGE}, 144 | new MPermissionUtils.OnPermissionListener() { 145 | @Override 146 | public void onPermissionGranted() { 147 | getPhotosTask.execute(); 148 | } 149 | 150 | @Override 151 | public void onPermissionDenied() { 152 | MPermissionUtils.showTipsDialog(PhotoPickerActivity.this); 153 | } 154 | }); 155 | 156 | // getPhotosTask.execute(); 157 | } 158 | 159 | private void initView() { 160 | mGridView = (GridView) findViewById(R.id.photo_gridview); 161 | mPhotoNumTV = (TextView) findViewById(R.id.photo_num); 162 | mPhotoNameTV = (TextView) findViewById(R.id.floder_name); 163 | findViewById(R.id.bottom_tab_bar).setOnTouchListener(new View.OnTouchListener() { 164 | @Override 165 | public boolean onTouch(View v, MotionEvent event) { 166 | //消费触摸事件,防止触摸底部tab栏也会选中图片 167 | return true; 168 | } 169 | }); 170 | findViewById(R.id.btn_back).setOnClickListener(new View.OnClickListener() { 171 | @Override 172 | public void onClick(View v) { 173 | finish(); 174 | } 175 | }); 176 | } 177 | 178 | /** 179 | * 初始化选项参数 180 | */ 181 | private void initIntentParams() { 182 | mIsShowCamera = getIntent().getBooleanExtra(EXTRA_SHOW_CAMERA, false); 183 | mSelectMode = getIntent().getIntExtra(EXTRA_SELECT_MODE, MODE_SINGLE); 184 | mMaxNum = getIntent().getIntExtra(EXTRA_MAX_MUN, DEFAULT_NUM); 185 | mTotalNum = getIntent().getIntExtra(TOTAL_MAX_MUN, 0); 186 | if (mSelectMode == MODE_MULTI) { 187 | //如果是多选模式,需要将确定按钮初始化以及绑定事件 188 | mCommitBtn = (Button) findViewById(R.id.commit); 189 | mCommitBtn.setVisibility(View.VISIBLE); 190 | mCommitBtn.setOnClickListener(new View.OnClickListener() { 191 | @Override 192 | public void onClick(View v) { 193 | mSelectList.addAll(mPhotoAdapter.getmSelectedPhotos()); 194 | returnData(); 195 | } 196 | }); 197 | } 198 | } 199 | 200 | /** 201 | * Android6.0检测权限 202 | */ 203 | private void checkPerm() { 204 | /**1.在AndroidManifest文件中添加需要的权限。 205 | * 206 | * 2.检查权限 207 | *这里涉及到一个API,ContextCompat.checkSelfPermission, 208 | * 主要用于检测某个权限是否已经被授予,方法返回值为PackageManager.PERMISSION_DENIED 209 | * 或者PackageManager.PERMISSION_GRANTED。当返回DENIED就需要进行申请授权了。 210 | * */ 211 | if (ContextCompat.checkSelfPermission(PhotoPickerActivity.this, Manifest.permission.CAMERA) 212 | != PackageManager.PERMISSION_GRANTED) { 213 | //权限没有被授予 214 | 215 | /**3.申请授权 216 | * @param 217 | * @param activity The target activity.(Activity|Fragment、) 218 | * @param permissions The requested permissions.(权限字符串数组) 219 | * @param requestCode Application specific request code to match with a result(int型申请码) 220 | * reported to {@link OnRequestPermissionsResultCallback#onRequestPermissionsResult( 221 | *int, String[], int[])}. 222 | * */ 223 | ActivityCompat.requestPermissions(this, 224 | new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, 225 | Manifest.permission.READ_EXTERNAL_STORAGE, 226 | Manifest.permission.CAMERA}, 227 | 1001); 228 | } else { 229 | //权限被授予 直接操作 230 | // choosePhoto(); 231 | getPhotosTask.execute(); 232 | } 233 | } 234 | 235 | /*** 236 | * 4.处理权限申请回调 237 | */ 238 | 239 | @Override 240 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { 241 | // if (requestCode == 1001) { 242 | // if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED 243 | // && grantResults[1] == PackageManager.PERMISSION_GRANTED 244 | // && grantResults[2] == PackageManager.PERMISSION_GRANTED) { 245 | // //权限被授予 246 | // Toast.makeText(PhotoPickerActivity.this, "Permission success", Toast.LENGTH_SHORT).show(); 247 | // getPhotosTask.execute(); 248 | // } else { 249 | // // Permission Denied 250 | // Toast.makeText(PhotoPickerActivity.this, "Permission Denied", Toast.LENGTH_SHORT).show(); 251 | // } 252 | // return; 253 | // } 254 | 255 | MPermissionUtils.onRequestPermissionsResult(requestCode, permissions, grantResults); 256 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 257 | } 258 | 259 | private void getPhotosSuccess() { 260 | mProgressDialog.dismiss(); 261 | mPhotoLists.addAll(mFolderMap.get(ALL_PHOTO).getPhotoList()); 262 | 263 | mPhotoNumTV.setText(OtherUtils.formatResourceString(getApplicationContext(), 264 | R.string.photos_num, mPhotoLists.size())); 265 | 266 | mPhotoAdapter = new PhotoAdapter(this.getApplicationContext(), mPhotoLists); 267 | mPhotoAdapter.setIsShowCamera(mIsShowCamera); 268 | mPhotoAdapter.setSelectMode(mSelectMode); 269 | mPhotoAdapter.setMaxNum(mMaxNum); 270 | mPhotoAdapter.setCurentNum(mTotalNum); // 设置当前已选择的图片数量 271 | mPhotoAdapter.setPhotoClickCallBack(this); 272 | mGridView.setAdapter(mPhotoAdapter); 273 | Set keys = mFolderMap.keySet(); 274 | final List folders = new ArrayList<>(); 275 | for (String key : keys) { 276 | if (ALL_PHOTO.equals(key)) { 277 | PhotoFolder folder = mFolderMap.get(key); 278 | folder.setIsSelected(true); 279 | folders.add(0, folder); 280 | } else { 281 | folders.add(mFolderMap.get(key)); 282 | } 283 | } 284 | mPhotoNameTV.setOnClickListener(new View.OnClickListener() { 285 | @TargetApi(Build.VERSION_CODES.KITKAT) 286 | @Override 287 | public void onClick(View v) { 288 | toggleFolderList(folders); 289 | } 290 | }); 291 | 292 | mGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 293 | @Override 294 | public void onItemClick(AdapterView parent, View view, int position, long id) { 295 | if (mPhotoAdapter.isShowCamera() && position == 0) { 296 | showCamera(); 297 | return; 298 | } 299 | selectPhoto(mPhotoAdapter.getItem(position)); 300 | } 301 | }); 302 | } 303 | 304 | /** 305 | * 点击选择某张照片 306 | * 307 | * @param photo 308 | */ 309 | private void selectPhoto(Photo photo) { 310 | if (photo == null) { 311 | return; 312 | } 313 | String path = photo.getPath(); 314 | if (mSelectMode == MODE_SINGLE) { 315 | mSelectList.add(path); 316 | returnData(); 317 | } 318 | } 319 | 320 | @Override 321 | public void onPhotoClick() { 322 | List list = mPhotoAdapter.getmSelectedPhotos(); 323 | if (list != null && list.size() > 0) { 324 | mCommitBtn.setEnabled(true); 325 | mCommitBtn.setText(OtherUtils.formatResourceString(getApplicationContext(), 326 | R.string.commit_num, list.size(), mMaxNum)); 327 | } else { 328 | mCommitBtn.setEnabled(false); 329 | mCommitBtn.setText(R.string.commit); 330 | } 331 | } 332 | 333 | /** 334 | * 返回选择图片的路径 335 | */ 336 | private void returnData() { 337 | // 返回已选择的图片数据 338 | Intent data = new Intent(); 339 | data.putStringArrayListExtra(KEY_RESULT, mSelectList); 340 | setResult(RESULT_OK, data); 341 | finish(); 342 | } 343 | 344 | /** 345 | * 显示或者隐藏文件夹列表 346 | * 347 | * @param folders 348 | */ 349 | private void toggleFolderList(final List folders) { 350 | //初始化文件夹列表 351 | if (!mIsFolderViewInit) { 352 | ViewStub folderStub = (ViewStub) findViewById(R.id.floder_stub); 353 | folderStub.inflate(); 354 | View dimLayout = findViewById(R.id.dim_layout); 355 | mFolderListView = (ListView) findViewById(R.id.listview_floder); 356 | final FolderAdapter adapter = new FolderAdapter(this, folders); 357 | mFolderListView.setAdapter(adapter); 358 | mFolderListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 359 | @Override 360 | public void onItemClick(AdapterView parent, View view, int position, long id) { 361 | for (PhotoFolder folder : folders) { 362 | folder.setIsSelected(false); 363 | } 364 | PhotoFolder folder = folders.get(position); 365 | folder.setIsSelected(true); 366 | adapter.notifyDataSetChanged(); 367 | 368 | mPhotoLists.clear(); 369 | mPhotoLists.addAll(folder.getPhotoList()); 370 | if (ALL_PHOTO.equals(folder.getName())) { 371 | mPhotoAdapter.setIsShowCamera(mIsShowCamera); 372 | } else { 373 | mPhotoAdapter.setIsShowCamera(false); 374 | } 375 | //这里重新设置adapter而不是直接notifyDataSetChanged,是让GridView返回顶部 376 | mGridView.setAdapter(mPhotoAdapter); 377 | mPhotoNumTV.setText(OtherUtils.formatResourceString(getApplicationContext(), 378 | R.string.photos_num, mPhotoLists.size())); 379 | mPhotoNameTV.setText(folder.getName()); 380 | toggle(); 381 | } 382 | }); 383 | dimLayout.setOnTouchListener(new View.OnTouchListener() { 384 | @Override 385 | public boolean onTouch(View v, MotionEvent event) { 386 | if (mIsFolderViewShow) { 387 | toggle(); 388 | return true; 389 | } else { 390 | return false; 391 | } 392 | } 393 | }); 394 | initAnimation(dimLayout); 395 | mIsFolderViewInit = true; 396 | } 397 | toggle(); 398 | } 399 | 400 | /** 401 | * 弹出或者收起文件夹列表 402 | */ 403 | private void toggle() { 404 | if (mIsFolderViewShow) { 405 | outAnimatorSet.start(); 406 | mIsFolderViewShow = false; 407 | } else { 408 | inAnimatorSet.start(); 409 | mIsFolderViewShow = true; 410 | } 411 | } 412 | 413 | /** 414 | * 初始化文件夹列表的显示隐藏动画 415 | */ 416 | AnimatorSet inAnimatorSet = new AnimatorSet(); 417 | AnimatorSet outAnimatorSet = new AnimatorSet(); 418 | 419 | private void initAnimation(View dimLayout) { 420 | ObjectAnimator alphaInAnimator, alphaOutAnimator, transInAnimator, transOutAnimator; 421 | //获取actionBar的高 422 | TypedValue tv = new TypedValue(); 423 | int actionBarHeight = 0; 424 | if (getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) { 425 | actionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data, getResources().getDisplayMetrics()); 426 | } 427 | /** 428 | * 这里的高度是,屏幕高度减去上、下tab栏,并且上面留有一个tab栏的高度 429 | * 所以这里减去3个actionBarHeight的高度 430 | */ 431 | int height = OtherUtils.getHeightInPx(this) - 3 * actionBarHeight; 432 | alphaInAnimator = ObjectAnimator.ofFloat(dimLayout, "alpha", 0f, 0.7f); 433 | alphaOutAnimator = ObjectAnimator.ofFloat(dimLayout, "alpha", 0.7f, 0f); 434 | transInAnimator = ObjectAnimator.ofFloat(mFolderListView, "translationY", height, 0); 435 | transOutAnimator = ObjectAnimator.ofFloat(mFolderListView, "translationY", 0, height); 436 | 437 | LinearInterpolator linearInterpolator = new LinearInterpolator(); 438 | 439 | inAnimatorSet.play(transInAnimator).with(alphaInAnimator); 440 | inAnimatorSet.setDuration(300); 441 | inAnimatorSet.setInterpolator(linearInterpolator); 442 | outAnimatorSet.play(transOutAnimator).with(alphaOutAnimator); 443 | outAnimatorSet.setDuration(300); 444 | outAnimatorSet.setInterpolator(linearInterpolator); 445 | } 446 | 447 | /** 448 | * 选择文件夹 449 | * 450 | * @param photoFolder 451 | */ 452 | public void selectFolder(PhotoFolder photoFolder) { 453 | mPhotoAdapter.setDatas(photoFolder.getPhotoList()); 454 | mPhotoAdapter.notifyDataSetChanged(); 455 | } 456 | 457 | /** 458 | * 获取照片的异步任务 459 | */ 460 | private AsyncTask getPhotosTask = new AsyncTask() { 461 | @Override 462 | protected void onPreExecute() { 463 | mProgressDialog = ProgressDialog.show(PhotoPickerActivity.this, null, "loading..."); 464 | } 465 | 466 | @Override 467 | protected Object doInBackground(Object[] params) { 468 | mFolderMap = PhotoUtils.getPhotos(PhotoPickerActivity.this.getApplicationContext()); 469 | return null; 470 | } 471 | 472 | @Override 473 | protected void onPostExecute(Object o) { 474 | getPhotosSuccess(); 475 | } 476 | }; 477 | 478 | /** 479 | * 选择相机 480 | */ 481 | private void showCamera() { 482 | // 跳转到系统照相机 483 | Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 484 | if (cameraIntent.resolveActivity(getPackageManager()) != null) { 485 | // 设置系统相机拍照后的输出路径 486 | // 创建临时文件 487 | mTmpFile = OtherUtils.createFile(getApplicationContext()); 488 | cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mTmpFile)); 489 | startActivityForResult(cameraIntent, REQUEST_CAMERA); 490 | } else { 491 | Toast.makeText(getApplicationContext(), 492 | R.string.msg_no_camera, Toast.LENGTH_SHORT).show(); 493 | } 494 | } 495 | 496 | @Override 497 | public void onActivityResult(int requestCode, int resultCode, Intent data) { 498 | // 相机拍照完成后,返回图片路径 499 | if (requestCode == REQUEST_CAMERA) { 500 | if (resultCode == Activity.RESULT_OK) { 501 | if (mTmpFile != null) { 502 | sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + mTmpFile.getAbsolutePath()))); 503 | mSelectList.add(mTmpFile.getAbsolutePath()); 504 | returnData(); 505 | } 506 | } else { 507 | if (mTmpFile != null && mTmpFile.exists()) { 508 | mTmpFile.delete(); 509 | } 510 | } 511 | } 512 | } 513 | 514 | } 515 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/PreviewPhotoActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.graphics.Bitmap; 7 | import android.graphics.Color; 8 | import android.os.Bundle; 9 | import android.support.v4.view.PagerAdapter; 10 | import android.support.v4.view.ViewPager.OnPageChangeListener; 11 | import android.view.View; 12 | import android.view.View.OnClickListener; 13 | import android.view.ViewGroup; 14 | import android.view.ViewGroup.LayoutParams; 15 | import android.widget.Button; 16 | import android.widget.ImageView; 17 | import android.widget.TextView; 18 | 19 | import com.example.mjj.selectphotodemo.utils.Bimp; 20 | import com.example.mjj.selectphotodemo.zoom.PhotoView; 21 | import com.example.mjj.selectphotodemo.zoom.ViewPagerFixed; 22 | 23 | import java.util.ArrayList; 24 | 25 | /** 26 | * Description:进行图片浏览,可删除 27 | *

28 | * Created by Mjj on 2016/12/2. 29 | */ 30 | 31 | public class PreviewPhotoActivity extends Activity { 32 | 33 | private Intent intent; 34 | // 返回图标 35 | private TextView back_bt; 36 | // 发送按钮 37 | private Button send_bt; 38 | //删除图标 39 | private ImageView del_bt; 40 | 41 | //获取前一个activity传过来的position 42 | private int position; 43 | //当前的位置 44 | private int location = 0; 45 | 46 | private ArrayList listViews = null; 47 | private ViewPagerFixed pager; 48 | private MyPageAdapter adapter; 49 | 50 | private Context mContext; 51 | 52 | @Override 53 | public void onCreate(Bundle savedInstanceState) { 54 | super.onCreate(savedInstanceState); 55 | setContentView(R.layout.plugin_camera_gallery);// 切屏到主界面 56 | mContext = this; 57 | back_bt = (TextView) findViewById(R.id.gallery_back); 58 | send_bt = (Button) findViewById(R.id.send_button); 59 | del_bt = (ImageView) findViewById(R.id.gallery_del); 60 | // 返回键监听 61 | back_bt.setOnClickListener(new OnClickListener() { 62 | @Override 63 | public void onClick(View view) { 64 | finish(); 65 | } 66 | }); 67 | send_bt.setOnClickListener(new GallerySendListener()); 68 | del_bt.setOnClickListener(new DelListener()); 69 | intent = getIntent(); 70 | position = Integer.parseInt(intent.getStringExtra("position")); 71 | isShowOkBt(); 72 | pager = (ViewPagerFixed) findViewById(R.id.gallery01); 73 | pager.addOnPageChangeListener(pageChangeListener); 74 | for (int i = 0; i < Bimp.tempSelectBitmap.size(); i++) { 75 | Bitmap tempBit = Bimp.tempSelectBitmap.get(i).getBitmap(); 76 | initListViews(tempBit); 77 | } 78 | 79 | adapter = new MyPageAdapter(listViews); 80 | pager.setAdapter(adapter); 81 | pager.setPageMargin(10); 82 | pager.setOffscreenPageLimit(9); 83 | 84 | int id = intent.getIntExtra("ID", 0); 85 | pager.setCurrentItem(id); 86 | } 87 | 88 | private OnPageChangeListener pageChangeListener = new OnPageChangeListener() { 89 | 90 | public void onPageSelected(int arg0) { 91 | location = arg0; 92 | } 93 | 94 | public void onPageScrolled(int arg0, float arg1, int arg2) { 95 | 96 | } 97 | 98 | public void onPageScrollStateChanged(int arg0) { 99 | 100 | } 101 | }; 102 | 103 | private void initListViews(Bitmap bm) { 104 | if (listViews == null) 105 | listViews = new ArrayList(); 106 | PhotoView img = new PhotoView(this); 107 | img.setImageBitmap(bm); 108 | img.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 109 | LayoutParams.MATCH_PARENT)); 110 | listViews.add(img); 111 | } 112 | 113 | // 删除按钮添加的监听器 114 | private class DelListener implements OnClickListener { 115 | 116 | public void onClick(View v) { 117 | if (listViews.size() == 1) { 118 | Bimp.tempSelectBitmap.clear(); 119 | Bimp.max = 0; 120 | back_bt.setText("" + Bimp.tempSelectBitmap.size() + " / 9"); 121 | Intent intent = new Intent("data.broadcast.action"); 122 | sendBroadcast(intent); 123 | finish(); 124 | } else { 125 | Bimp.tempSelectBitmap.remove(location); 126 | Bimp.max--; 127 | pager.removeAllViews(); 128 | listViews.remove(location); 129 | adapter.setListViews(listViews); 130 | back_bt.setText("" + Bimp.tempSelectBitmap.size() + " / 9"); 131 | adapter.notifyDataSetChanged(); 132 | } 133 | } 134 | } 135 | 136 | // 完成按钮的监听 137 | private class GallerySendListener implements OnClickListener { 138 | public void onClick(View v) { 139 | finish(); 140 | intent.setClass(mContext, FollowWeChatPhotoActivity.class); 141 | startActivity(intent); 142 | } 143 | } 144 | 145 | public void isShowOkBt() { 146 | if (Bimp.tempSelectBitmap.size() > 0) { 147 | back_bt.setText("" + Bimp.tempSelectBitmap.size() + " / 9"); 148 | send_bt.setPressed(true); 149 | send_bt.setClickable(true); 150 | send_bt.setTextColor(Color.WHITE); 151 | } else { 152 | send_bt.setPressed(false); 153 | send_bt.setClickable(false); 154 | send_bt.setTextColor(Color.parseColor("#E1E0DE")); 155 | } 156 | } 157 | 158 | class MyPageAdapter extends PagerAdapter { 159 | 160 | private ArrayList listViews; 161 | 162 | private int size; 163 | 164 | public MyPageAdapter(ArrayList listViews) { 165 | this.listViews = listViews; 166 | size = listViews == null ? 0 : listViews.size(); 167 | } 168 | 169 | public void setListViews(ArrayList listViews) { 170 | this.listViews = listViews; 171 | size = listViews == null ? 0 : listViews.size(); 172 | } 173 | 174 | public int getCount() { 175 | return size; 176 | } 177 | 178 | public int getItemPosition(Object object) { 179 | return POSITION_NONE; 180 | } 181 | 182 | @Override 183 | public void destroyItem(ViewGroup container, int position, Object object) { 184 | container.removeView(listViews.get(position % size)); 185 | } 186 | 187 | @Override 188 | public Object instantiateItem(ViewGroup container, int position) { 189 | try { 190 | container.addView(listViews.get(position % size), 0); 191 | } catch (Exception e) { 192 | } 193 | return listViews.get(position % size); 194 | } 195 | 196 | public boolean isViewFromObject(View arg0, Object arg1) { 197 | return arg0 == arg1; 198 | } 199 | } 200 | 201 | } 202 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/adapters/FolderAdapter.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.adapters; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.BaseAdapter; 8 | import android.widget.ImageView; 9 | import android.widget.TextView; 10 | 11 | import com.example.mjj.selectphotodemo.R; 12 | import com.example.mjj.selectphotodemo.beans.PhotoFolder; 13 | import com.example.mjj.selectphotodemo.utils.ImageLoader; 14 | import com.example.mjj.selectphotodemo.utils.OtherUtils; 15 | 16 | import java.util.List; 17 | 18 | /** 19 | * Description: 相册目录列表适配器 20 | *

21 | * Created by Mjj on 2016/12/2. 22 | */ 23 | public class FolderAdapter extends BaseAdapter { 24 | 25 | List mDatas; 26 | Context mContext; 27 | int mWidth; 28 | 29 | public FolderAdapter(Context context, List mDatas) { 30 | this.mDatas = mDatas; 31 | this.mContext = context; 32 | mWidth = OtherUtils.dip2px(context, 90); 33 | } 34 | 35 | @Override 36 | public int getCount() { 37 | if (mDatas == null) { 38 | return 0; 39 | } 40 | return mDatas.size(); 41 | } 42 | 43 | @Override 44 | public PhotoFolder getItem(int position) { 45 | if (mDatas == null || mDatas.size() == 0) { 46 | return null; 47 | } 48 | return mDatas.get(position); 49 | } 50 | 51 | @Override 52 | public long getItemId(int position) { 53 | return position; 54 | } 55 | 56 | @Override 57 | public View getView(int position, View convertView, ViewGroup parent) { 58 | ViewHolder holder; 59 | if (convertView == null) { 60 | holder = new ViewHolder(); 61 | convertView = LayoutInflater.from(mContext).inflate( 62 | R.layout.item_folder_layout, null); 63 | holder.photoIV = (ImageView) convertView.findViewById(R.id.imageview_folder_img); 64 | holder.folderNameTV = (TextView) convertView.findViewById(R.id.textview_folder_name); 65 | holder.photoNumTV = (TextView) convertView.findViewById(R.id.textview_photo_num); 66 | holder.selectIV = (ImageView) convertView.findViewById(R.id.imageview_folder_select); 67 | convertView.setTag(holder); 68 | } else { 69 | holder = (ViewHolder) convertView.getTag(); 70 | } 71 | PhotoFolder folder = getItem(position); 72 | if (folder == null) { 73 | return convertView; 74 | } 75 | if (folder.getPhotoList() == null || folder.getPhotoList().size() == 0) { 76 | return convertView; 77 | } 78 | holder.selectIV.setVisibility(View.GONE); 79 | holder.photoIV.setImageResource(R.drawable.ic_photo_loading); 80 | if (folder.isSelected()) { 81 | holder.selectIV.setVisibility(View.VISIBLE); 82 | } 83 | holder.folderNameTV.setText(folder.getName()); 84 | holder.photoNumTV.setText(folder.getPhotoList().size() + "张"); 85 | ImageLoader.getInstance().display(folder.getPhotoList().get(0).getPath(), holder.photoIV, 86 | mWidth, mWidth); 87 | return convertView; 88 | } 89 | 90 | private class ViewHolder { 91 | private ImageView photoIV; 92 | private TextView folderNameTV; 93 | private TextView photoNumTV; 94 | private ImageView selectIV; 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/adapters/PhotoAdapter.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.adapters; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.BaseAdapter; 9 | import android.widget.FrameLayout; 10 | import android.widget.GridView; 11 | import android.widget.ImageView; 12 | import android.widget.Toast; 13 | 14 | import com.example.mjj.selectphotodemo.PhotoPickerActivity; 15 | import com.example.mjj.selectphotodemo.R; 16 | import com.example.mjj.selectphotodemo.beans.Photo; 17 | import com.example.mjj.selectphotodemo.utils.ImageLoader; 18 | import com.example.mjj.selectphotodemo.utils.OtherUtils; 19 | 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | /** 24 | * Description:图片选择界面GridView适配器 25 | *

26 | * Created by Mjj on 2016/12/2. 27 | */ 28 | public class PhotoAdapter extends BaseAdapter { 29 | 30 | private static final int TYPE_CAMERA = 0; 31 | private static final int TYPE_PHOTO = 1; 32 | 33 | private List mDatas; 34 | //存放已选中的Photo数据 35 | private List mSelectedPhotos; 36 | private Context mContext; 37 | private int mWidth; 38 | //是否显示相机,默认不显示 39 | private boolean mIsShowCamera = false; 40 | //照片选择模式,默认单选 41 | private int mSelectMode = PhotoPickerActivity.MODE_SINGLE; 42 | //图片选择数量 43 | private int mMaxNum = PhotoPickerActivity.DEFAULT_NUM; 44 | 45 | private View.OnClickListener mOnPhotoClick; 46 | private PhotoClickCallBack mCallBack; 47 | 48 | private int curentNum; // 当前已选择的图片数量 49 | 50 | public int getCurentNum() { 51 | return curentNum; 52 | } 53 | 54 | public void setCurentNum(int curentNum) { 55 | this.curentNum = curentNum; 56 | } 57 | 58 | public PhotoAdapter(Context context, List mDatas) { 59 | this.mDatas = mDatas; 60 | this.mContext = context; 61 | int screenWidth = OtherUtils.getWidthInPx(mContext); 62 | mWidth = (screenWidth - OtherUtils.dip2px(mContext, 4)) / 3; 63 | } 64 | 65 | @Override 66 | public int getViewTypeCount() { 67 | return 2; 68 | } 69 | 70 | @Override 71 | public int getItemViewType(int position) { 72 | if (getItem(position) != null && getItem(position).isCamera()) { 73 | return TYPE_CAMERA; 74 | } else { 75 | return TYPE_PHOTO; 76 | } 77 | } 78 | 79 | @Override 80 | public int getCount() { 81 | return mDatas.size(); 82 | } 83 | 84 | @Override 85 | public Photo getItem(int position) { 86 | if (mDatas == null || mDatas.size() == 0) { 87 | return null; 88 | } 89 | return mDatas.get(position); 90 | } 91 | 92 | @Override 93 | public long getItemId(int position) { 94 | return mDatas.get(position).getId(); 95 | } 96 | 97 | public void setDatas(List mDatas) { 98 | this.mDatas = mDatas; 99 | } 100 | 101 | public boolean isShowCamera() { 102 | return mIsShowCamera; 103 | } 104 | 105 | public void setIsShowCamera(boolean isShowCamera) { 106 | this.mIsShowCamera = isShowCamera; 107 | if (mIsShowCamera) { 108 | Photo camera = new Photo(null); 109 | camera.setIsCamera(true); 110 | mDatas.add(0, camera); 111 | } 112 | } 113 | 114 | public void setMaxNum(int maxNum) { 115 | this.mMaxNum = maxNum; 116 | } 117 | 118 | public void setPhotoClickCallBack(PhotoClickCallBack callback) { 119 | mCallBack = callback; 120 | } 121 | 122 | /** 123 | * 获取已选中相片 124 | * 125 | * @return 126 | */ 127 | public List getmSelectedPhotos() { 128 | return mSelectedPhotos; 129 | } 130 | 131 | public void setSelectMode(int selectMode) { 132 | this.mSelectMode = selectMode; 133 | if (mSelectMode == PhotoPickerActivity.MODE_MULTI) { 134 | initMultiMode(); 135 | } 136 | } 137 | 138 | /** 139 | * 初始化多选模式所需要的参数 140 | */ 141 | private void initMultiMode() { 142 | mSelectedPhotos = new ArrayList<>(); 143 | mOnPhotoClick = new View.OnClickListener() { 144 | @Override 145 | public void onClick(View v) { 146 | String path = v.findViewById(R.id.imageview_photo).getTag().toString(); 147 | if (mSelectedPhotos.contains(path)) { 148 | v.findViewById(R.id.mask).setVisibility(View.GONE); 149 | v.findViewById(R.id.checkmark).setSelected(false); 150 | mSelectedPhotos.remove(path); 151 | } else { 152 | if (mSelectedPhotos.size() >= mMaxNum) { 153 | Toast.makeText(mContext, R.string.msg_maxi_capacity, 154 | Toast.LENGTH_SHORT).show(); 155 | return; 156 | } 157 | // 判断显示总共的数量 158 | mSelectedPhotos.add(path); 159 | int num = mSelectedPhotos.size() + getCurentNum(); 160 | Log.e("PhotoAdapter999", "selectPhoto: " + mSelectedPhotos.size() + " ********* " + getCurentNum()); 161 | if (num > 9) { 162 | mSelectedPhotos.remove(mSelectedPhotos.size() - 1); 163 | Toast.makeText(mContext, "图片数量已达上限", Toast.LENGTH_SHORT).show(); 164 | return; 165 | } 166 | v.findViewById(R.id.mask).setVisibility(View.VISIBLE); 167 | v.findViewById(R.id.checkmark).setSelected(true); 168 | } 169 | if (mCallBack != null) { 170 | mCallBack.onPhotoClick(); 171 | } 172 | } 173 | }; 174 | } 175 | 176 | @Override 177 | public View getView(int position, View convertView, ViewGroup parent) { 178 | if (getItemViewType(position) == TYPE_CAMERA) { 179 | convertView = LayoutInflater.from(mContext).inflate( 180 | R.layout.item_camera_layout, null); 181 | convertView.setTag(null); 182 | //设置高度等于宽度 183 | GridView.LayoutParams lp = new GridView.LayoutParams(mWidth, mWidth); 184 | convertView.setLayoutParams(lp); 185 | } else { 186 | ViewHolder holder; 187 | if (convertView == null) { 188 | holder = new ViewHolder(); 189 | convertView = LayoutInflater.from(mContext).inflate( 190 | R.layout.item_photo_layout, null); 191 | holder.photoImageView = (ImageView) convertView.findViewById(R.id.imageview_photo); 192 | holder.selectView = (ImageView) convertView.findViewById(R.id.checkmark); 193 | holder.maskView = convertView.findViewById(R.id.mask); 194 | holder.wrapLayout = (FrameLayout) convertView.findViewById(R.id.wrap_layout); 195 | convertView.setTag(holder); 196 | } else { 197 | holder = (ViewHolder) convertView.getTag(); 198 | } 199 | 200 | holder.photoImageView.setImageResource(R.drawable.ic_photo_loading); 201 | Photo photo = getItem(position); 202 | if (mSelectMode == PhotoPickerActivity.MODE_MULTI) { 203 | holder.wrapLayout.setOnClickListener(mOnPhotoClick); 204 | holder.photoImageView.setTag(photo.getPath()); 205 | holder.selectView.setVisibility(View.VISIBLE); 206 | if (mSelectedPhotos != null && mSelectedPhotos.contains(photo.getPath())) { 207 | holder.selectView.setSelected(true); 208 | holder.maskView.setVisibility(View.VISIBLE); 209 | } else { 210 | holder.selectView.setSelected(false); 211 | holder.maskView.setVisibility(View.GONE); 212 | } 213 | } else { 214 | holder.selectView.setVisibility(View.GONE); 215 | } 216 | ImageLoader.getInstance().display(photo.getPath(), holder.photoImageView, 217 | mWidth, mWidth); 218 | } 219 | return convertView; 220 | } 221 | 222 | private class ViewHolder { 223 | private ImageView photoImageView; 224 | private ImageView selectView; 225 | private View maskView; 226 | private FrameLayout wrapLayout; 227 | } 228 | 229 | /** 230 | * 多选时,点击相片的回调接口 231 | */ 232 | public interface PhotoClickCallBack { 233 | void onPhotoClick(); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/beans/ImageItem.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.beans; 2 | 3 | import android.graphics.Bitmap; 4 | 5 | import com.example.mjj.selectphotodemo.utils.Bimp; 6 | 7 | import java.io.IOException; 8 | import java.io.Serializable; 9 | 10 | /** 11 | * Description:单个图片实体类 12 | *

13 | * Created by Mjj on 2016/12/2. 14 | */ 15 | 16 | public class ImageItem implements Serializable { 17 | 18 | public String imageId; 19 | public String thumbnailPath; 20 | public String imagePath; 21 | private Bitmap bitmap; 22 | public boolean isSelected = false; 23 | 24 | public String getImageId() { 25 | return imageId; 26 | } 27 | 28 | public void setImageId(String imageId) { 29 | this.imageId = imageId; 30 | } 31 | 32 | public String getThumbnailPath() { 33 | return thumbnailPath; 34 | } 35 | 36 | public void setThumbnailPath(String thumbnailPath) { 37 | this.thumbnailPath = thumbnailPath; 38 | } 39 | 40 | public String getImagePath() { 41 | return imagePath; 42 | } 43 | 44 | public void setImagePath(String imagePath) { 45 | this.imagePath = imagePath; 46 | } 47 | 48 | public boolean isSelected() { 49 | return isSelected; 50 | } 51 | 52 | public void setSelected(boolean isSelected) { 53 | this.isSelected = isSelected; 54 | } 55 | 56 | public Bitmap getBitmap() { 57 | if (bitmap == null) { 58 | try { 59 | bitmap = Bimp.revitionImageSize(imagePath); 60 | } catch (IOException e) { 61 | e.printStackTrace(); 62 | } 63 | } 64 | return bitmap; 65 | } 66 | 67 | public void setBitmap(Bitmap bitmap) { 68 | this.bitmap = bitmap; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/beans/Photo.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.beans; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Description:图片选择界面图片实体 7 | *

8 | * Created by Mjj on 2016/12/2. 9 | */ 10 | public class Photo implements Serializable { 11 | 12 | private int id; 13 | private String path; //路径 14 | private boolean isCamera; 15 | 16 | public Photo(String path) { 17 | this.path = path; 18 | } 19 | 20 | public int getId() { 21 | return id; 22 | } 23 | 24 | public void setId(int id) { 25 | this.id = id; 26 | } 27 | 28 | public String getPath() { 29 | return path; 30 | } 31 | 32 | public void setPath(String path) { 33 | this.path = path; 34 | } 35 | 36 | public boolean isCamera() { 37 | return isCamera; 38 | } 39 | 40 | public void setIsCamera(boolean isCamera) { 41 | this.isCamera = isCamera; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/beans/PhotoFolder.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.beans; 2 | 3 | import java.io.Serializable; 4 | import java.util.List; 5 | 6 | /** 7 | * Description:相册列表实体 8 | *

9 | * Created by Mjj on 2016/12/2. 10 | */ 11 | 12 | public class PhotoFolder implements Serializable { 13 | 14 | // 文件夹名 15 | private String name; 16 | // 文件夹路径 17 | private String dirPath; 18 | // 该文件夹下图片列表 19 | private List photoList; 20 | // 标识是否选中该文件夹 21 | private boolean isSelected; 22 | 23 | public boolean isSelected() { 24 | return isSelected; 25 | } 26 | 27 | public void setIsSelected(boolean isSelected) { 28 | this.isSelected = isSelected; 29 | } 30 | 31 | public String getName() { 32 | return name; 33 | } 34 | 35 | public void setName(String name) { 36 | this.name = name; 37 | } 38 | 39 | public String getDirPath() { 40 | return dirPath; 41 | } 42 | 43 | public void setDirPath(String dirPath) { 44 | this.dirPath = dirPath; 45 | } 46 | 47 | public List getPhotoList() { 48 | return photoList; 49 | } 50 | 51 | public void setPhotoList(List photoList) { 52 | this.photoList = photoList; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/utils/Bimp.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.utils; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.BitmapFactory; 5 | 6 | import com.example.mjj.selectphotodemo.beans.ImageItem; 7 | 8 | import java.io.BufferedInputStream; 9 | import java.io.File; 10 | import java.io.FileInputStream; 11 | import java.io.IOException; 12 | import java.util.ArrayList; 13 | 14 | /** 15 | * Description:出来临时图片数据 16 | *

17 | * Created by Mjj on 2016/12/2. 18 | */ 19 | 20 | public class Bimp { 21 | 22 | public static int max = 0; 23 | 24 | public static ArrayList tempSelectBitmap = new ArrayList(); // 图片临时列表 25 | 26 | public static Bitmap revitionImageSize(String path) throws IOException { 27 | BufferedInputStream in = new BufferedInputStream(new FileInputStream( 28 | new File(path))); 29 | BitmapFactory.Options options = new BitmapFactory.Options(); 30 | options.inJustDecodeBounds = true; 31 | BitmapFactory.decodeStream(in, null, options); 32 | in.close(); 33 | int i = 0; 34 | Bitmap bitmap = null; 35 | while (true) { 36 | if ((options.outWidth >> i <= 1000) 37 | && (options.outHeight >> i <= 1000)) { 38 | in = new BufferedInputStream( 39 | new FileInputStream(new File(path))); 40 | options.inSampleSize = (int) Math.pow(2.0D, i); 41 | options.inJustDecodeBounds = false; 42 | bitmap = BitmapFactory.decodeStream(in, null, options); 43 | break; 44 | } 45 | i += 1; 46 | } 47 | return bitmap; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/utils/BitmapUtils.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.utils; 2 | 3 | import android.content.res.Resources; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | 7 | /** 8 | * Description:图片压缩 9 | *

10 | * Created by Mjj on 2016/12/3. 11 | */ 12 | 13 | public class BitmapUtils { 14 | 15 | private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { 16 | final int height = options.outHeight; 17 | final int width = options.outWidth; 18 | int inSampleSize = 1; 19 | if (height > reqHeight || width > reqWidth) { 20 | final int halfHeight = height / 2; 21 | final int halfWidth = width / 2; 22 | while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { 23 | inSampleSize *= 2; 24 | } 25 | } 26 | return inSampleSize; 27 | } 28 | 29 | /** 30 | * 根据Resources压缩图片 31 | * 32 | * @param res 33 | * @param resId 34 | * @param reqWidth 35 | * @param reqHeight 36 | * @return 37 | */ 38 | public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { 39 | final BitmapFactory.Options options = new BitmapFactory.Options(); 40 | options.inJustDecodeBounds = true; 41 | BitmapFactory.decodeResource(res, resId, options); 42 | options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); 43 | options.inJustDecodeBounds = false; 44 | Bitmap src = BitmapFactory.decodeResource(res, resId, options); 45 | return src; 46 | } 47 | 48 | /** 49 | * 根据地址压缩图片 50 | * 51 | * @param pathName 52 | * @param reqWidth 53 | * @param reqHeight 54 | * @return 55 | */ 56 | public static Bitmap decodeSampledBitmapFromFd(String pathName, int reqWidth, int reqHeight) { 57 | final BitmapFactory.Options options = new BitmapFactory.Options(); 58 | options.inJustDecodeBounds = true; 59 | BitmapFactory.decodeFile(pathName, options); 60 | options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); 61 | options.inJustDecodeBounds = false; 62 | Bitmap src = BitmapFactory.decodeFile(pathName, options); 63 | return src; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/utils/ImageLoader.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.utils; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.BitmapFactory; 5 | import android.os.AsyncTask; 6 | import android.os.Handler; 7 | import android.os.Looper; 8 | import android.os.Message; 9 | import android.text.TextUtils; 10 | import android.util.LruCache; 11 | import android.widget.ImageView; 12 | 13 | import com.example.mjj.selectphotodemo.Application; 14 | import com.example.mjj.selectphotodemo.io.DiskLruCache; 15 | 16 | import java.io.File; 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.io.OutputStream; 20 | import java.lang.ref.WeakReference; 21 | import java.util.LinkedList; 22 | import java.util.concurrent.Executor; 23 | import java.util.concurrent.Executors; 24 | import java.util.concurrent.Semaphore; 25 | 26 | /** 27 | * Description:图片缓存封装 28 | *

29 | * Created by Mjj on 2016/12/2. 30 | */ 31 | 32 | public class ImageLoader { 33 | 34 | private static final int THREAD_POOL_SIZE = 10; 35 | private final static Executor BITMAP_LOAD_EXECUTOR = Executors.newFixedThreadPool(THREAD_POOL_SIZE); 36 | private final static Executor BITMAP_CACHE_EXECUTOR = Executors.newFixedThreadPool(THREAD_POOL_SIZE); 37 | 38 | public static final Bitmap.CompressFormat DEFAULT_COMPRESS_FORMAT = Bitmap.CompressFormat.PNG; 39 | 40 | public static final int DEFAULT_COMPRESS_QUALITY = 100; 41 | 42 | private LruCache mMemoryCache; 43 | DiskLruCache mDiskLruCache = null; 44 | //图片加载任务队列 45 | private LinkedList mTaskQueue; 46 | private volatile Semaphore mPoolSemaphore; 47 | private Handler mHandler; 48 | 49 | private Thread mPoolThread; 50 | private Handler mPoolThreadHander; 51 | private volatile Semaphore mSemaphore = new Semaphore(0); 52 | private static ImageLoader mInstance; 53 | private int mWidth; 54 | 55 | private ImageLoader() { 56 | init(); 57 | } 58 | 59 | private void init() { 60 | initMemoryCache(); 61 | initDiskCache(); 62 | mHandler = new Handler() { 63 | @Override 64 | public void handleMessage(Message msg) { 65 | ImageHolder holder = (ImageHolder) msg.obj; 66 | String path = holder.path; 67 | ImageView imageView = holder.imageView; 68 | Bitmap bitmap = holder.bitmap; 69 | if (imageView == null || bitmap == null) { 70 | return; 71 | } 72 | if (!TextUtils.isEmpty(path) && path.equals(imageView.getTag().toString())) { 73 | imageView.setImageBitmap(bitmap); 74 | } 75 | } 76 | }; 77 | 78 | mPoolThread = new Thread() { 79 | @Override 80 | public void run() { 81 | Looper.prepare(); 82 | mPoolThreadHander = new Handler() { 83 | @Override 84 | public void handleMessage(Message msg) { 85 | try { 86 | mPoolSemaphore.acquire(); 87 | } catch (InterruptedException e) { 88 | } 89 | BitmapLoadTask task = getTask(); 90 | if (task != null) { 91 | task.executeOnExecutor(BITMAP_LOAD_EXECUTOR, mWidth, mWidth); 92 | } 93 | } 94 | }; 95 | // 释放一个信号量,告知mPoolThreadHander对象已经创建完成 96 | mSemaphore.release(); 97 | Looper.loop(); 98 | } 99 | }; 100 | mPoolThread.start(); 101 | 102 | mTaskQueue = new LinkedList(); 103 | mPoolSemaphore = new Semaphore(THREAD_POOL_SIZE); 104 | } 105 | 106 | public static synchronized ImageLoader getInstance() { 107 | if (mInstance == null) { 108 | mInstance = new ImageLoader(); 109 | } 110 | return mInstance; 111 | } 112 | 113 | /** 114 | * 初始化内存缓存 115 | */ 116 | public void initMemoryCache() { 117 | 118 | // Set up memory cache 119 | if (mMemoryCache != null) { 120 | try { 121 | clearMemoryCache(); 122 | } catch (Throwable e) { 123 | } 124 | } 125 | // find the max memory size of the system 126 | int maxMemory = (int) Runtime.getRuntime().maxMemory(); 127 | int cacheSize = maxMemory / 8; 128 | mMemoryCache = new LruCache(cacheSize) { 129 | 130 | @Override 131 | protected int sizeOf(String key, Bitmap bitmap) { 132 | if (bitmap == null) return 0; 133 | return bitmap.getRowBytes() * bitmap.getHeight(); 134 | } 135 | }; 136 | } 137 | 138 | /** 139 | * 初始化磁盘缓存 140 | */ 141 | private void initDiskCache() { 142 | try { 143 | File cacheDir = OtherUtils.getDiskCacheDir(Application.getContext(), "images"); 144 | if (!cacheDir.exists()) { 145 | cacheDir.mkdirs(); 146 | } 147 | mDiskLruCache = DiskLruCache.open(cacheDir, OtherUtils.getAppVersion(Application.getContext()), 148 | 1, 15 * 1024 * 1024); 149 | } catch (IOException e) { 150 | e.printStackTrace(); 151 | } 152 | } 153 | 154 | public void display(String path, ImageView imageView, int width, int height) { 155 | if (TextUtils.isEmpty(path) || imageView == null) { 156 | throw new IllegalArgumentException("args may not be null"); 157 | } 158 | mWidth = width; 159 | imageView.setTag(path); 160 | Bitmap bitmap = getBitmapFromMemoryCache(path); 161 | if (bitmap == null) { 162 | //从文件中加载 163 | BitmapLoadTask bitmapLoadTask = new BitmapLoadTask(path, imageView); 164 | addTask(bitmapLoadTask); 165 | } else { 166 | ImageHolder imageHolder = new ImageHolder(); 167 | imageHolder.bitmap = bitmap; 168 | imageHolder.imageView = imageView; 169 | imageHolder.path = path; 170 | Message msg = Message.obtain(); 171 | msg.obj = imageHolder; 172 | mHandler.sendMessage(msg); 173 | } 174 | } 175 | 176 | private synchronized void addTask(BitmapLoadTask task) { 177 | try { 178 | // 如果mPoolThreadHander为空,则阻塞等待mPoolThreadHander创建完毕 179 | if (mPoolThreadHander == null) { 180 | mSemaphore.acquire(); 181 | } 182 | } catch (InterruptedException e) { 183 | } 184 | mTaskQueue.add(task); 185 | mPoolThreadHander.sendEmptyMessage(0); 186 | } 187 | 188 | private synchronized BitmapLoadTask getTask() { 189 | return mTaskQueue.removeLast(); 190 | } 191 | 192 | /** 193 | * 从内存缓存中获取图片 194 | * 195 | * @param key 196 | * @return 197 | */ 198 | private Bitmap getBitmapFromMemoryCache(String key) { 199 | return mMemoryCache.get(key); 200 | } 201 | 202 | private void addBitmapToMemoryCache(String key, Bitmap bitmap) { 203 | if (!TextUtils.isEmpty(key) && bitmap != null) { 204 | mMemoryCache.put(key, bitmap); 205 | } 206 | } 207 | 208 | /** 209 | * 清空内存缓存 210 | */ 211 | public void clearMemoryCache() { 212 | if (mMemoryCache != null) { 213 | mMemoryCache.evictAll(); 214 | } 215 | } 216 | 217 | private class ImageHolder { 218 | Bitmap bitmap; 219 | ImageView imageView; 220 | String path; 221 | } 222 | 223 | public class BitmapLoadTask extends AsyncTask { 224 | 225 | private final String path; 226 | private final WeakReference containerReference; 227 | 228 | public BitmapLoadTask(String path, ImageView container) { 229 | if (container == null || path == null) { 230 | throw new IllegalArgumentException("args may not be null"); 231 | } 232 | this.path = path; 233 | this.containerReference = new WeakReference(container); 234 | } 235 | 236 | @Override 237 | protected Bitmap doInBackground(Integer... params) { 238 | Bitmap bitmap = null; 239 | bitmap = tryLoadFromDiskCache(path); 240 | 241 | if (bitmap == null) { 242 | bitmap = decodeSampledBitmapFromFile(path, params[0], 243 | params[1]); 244 | BITMAP_CACHE_EXECUTOR.execute(new DiskCacheThread(path, bitmap)); 245 | } 246 | mPoolSemaphore.release(); 247 | addBitmapToMemoryCache(path, bitmap); 248 | bitmap = getBitmapFromMemoryCache(path); 249 | return bitmap; 250 | } 251 | 252 | @Override 253 | protected void onCancelled(Bitmap bitmap) { 254 | 255 | } 256 | 257 | @Override 258 | protected void onPostExecute(Bitmap bitmap) { 259 | ImageHolder imageHolder = new ImageHolder(); 260 | imageHolder.bitmap = bitmap; 261 | imageHolder.imageView = containerReference.get(); 262 | imageHolder.path = path; 263 | Message msg = Message.obtain(); 264 | msg.obj = imageHolder; 265 | mHandler.sendMessage(msg); 266 | } 267 | } 268 | 269 | class DiskCacheThread extends Thread { 270 | 271 | String path; 272 | Bitmap bitmap; 273 | 274 | public DiskCacheThread(String path, Bitmap bitmap) { 275 | this.path = path; 276 | this.bitmap = bitmap; 277 | } 278 | 279 | @Override 280 | public void run() { 281 | try { 282 | String key = OtherUtils.hashKeyForDisk(path); 283 | DiskLruCache.Editor editor = mDiskLruCache.edit(key); 284 | if (editor != null) { 285 | OutputStream outputStream = editor.newOutputStream(0); 286 | if (cacheBitmap2Disk(bitmap, outputStream)) { 287 | editor.commit(); 288 | } else { 289 | editor.abort(); 290 | } 291 | } 292 | mDiskLruCache.flush(); 293 | } catch (IOException e) { 294 | e.printStackTrace(); 295 | } 296 | } 297 | } 298 | 299 | private Bitmap tryLoadFromDiskCache(String path) { 300 | Bitmap bitmap = null; 301 | try { 302 | String key = OtherUtils.hashKeyForDisk(path); 303 | DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); 304 | if (snapShot != null) { 305 | InputStream is = snapShot.getInputStream(0); 306 | bitmap = BitmapFactory.decodeStream(is); 307 | } 308 | } catch (IOException e) { 309 | e.printStackTrace(); 310 | } 311 | return bitmap; 312 | } 313 | 314 | private boolean cacheBitmap2Disk(Bitmap bitmap, OutputStream outputStream) { 315 | bitmap.compress(DEFAULT_COMPRESS_FORMAT, DEFAULT_COMPRESS_QUALITY, outputStream); 316 | return true; 317 | } 318 | 319 | 320 | /** 321 | * 计算inSampleSize,用于压缩图片 322 | * 323 | * @param options 324 | * @param reqWidth 325 | * @param reqHeight 326 | * @return 327 | */ 328 | private int calculateInSampleSize(BitmapFactory.Options options, 329 | int reqWidth, int reqHeight) { 330 | // 源图片的宽度 331 | int width = options.outWidth; 332 | int height = options.outHeight; 333 | int inSampleSize = 1; 334 | 335 | // 通过之前的计算方法,在加载类似400*4000这种长图时会内存溢出 336 | if (width > reqWidth || height > reqHeight) { 337 | int widthRadio = Math.round(width * 1.0f / reqWidth); 338 | int heightRadio = Math.round(height * 1.0f / reqHeight); 339 | 340 | inSampleSize = Math.max(widthRadio, heightRadio); 341 | } 342 | 343 | return inSampleSize; 344 | } 345 | 346 | /** 347 | * 根据计算的inSampleSize,得到压缩后图片 348 | * 349 | * @param pathName 350 | * @param reqWidth 351 | * @param reqHeight 352 | * @return 353 | */ 354 | private Bitmap decodeSampledBitmapFromFile(String pathName, 355 | int reqWidth, int reqHeight) { 356 | // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小 357 | final BitmapFactory.Options options = new BitmapFactory.Options(); 358 | options.inJustDecodeBounds = true; 359 | BitmapFactory.decodeFile(pathName, options); 360 | // 调用上面定义的方法计算inSampleSize值 361 | options.inSampleSize = calculateInSampleSize(options, reqWidth, 362 | reqHeight); 363 | // 使用获取到的inSampleSize值再次解析图片 364 | options.inJustDecodeBounds = false; 365 | Bitmap bitmap = BitmapFactory.decodeFile(pathName, options); 366 | 367 | return bitmap; 368 | } 369 | 370 | } 371 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/utils/MPermissionUtils: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.utils; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.content.DialogInterface; 7 | import android.content.Intent; 8 | import android.content.pm.PackageManager; 9 | import android.net.Uri; 10 | import android.os.Build; 11 | import android.provider.Settings; 12 | import android.support.v4.content.ContextCompat; 13 | import android.support.v7.app.AlertDialog; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | /** 19 | * Description:Android6.0运行时权限工具类 20 | *

21 | * Created by code小生 on 2016/12/30. 22 | */ 23 | 24 | public class MPermissionUtils { 25 | 26 | private static int mRequestCode = -1; 27 | 28 | public static void requestPermissionsResult(Activity activity, int requestCode 29 | , String[] permission, OnPermissionListener callback) { 30 | requestPermissions(activity, requestCode, permission, callback); 31 | } 32 | 33 | public static void requestPermissionsResult(android.app.Fragment fragment, int requestCode 34 | , String[] permission, OnPermissionListener callback) { 35 | requestPermissions(fragment, requestCode, permission, callback); 36 | } 37 | 38 | public static void requestPermissionsResult(android.support.v4.app.Fragment fragment, int requestCode 39 | , String[] permission, OnPermissionListener callback) { 40 | requestPermissions(fragment, requestCode, permission, callback); 41 | } 42 | 43 | /** 44 | * 请求权限处理 45 | * 46 | * @param object activity or fragment 47 | * @param requestCode 请求码 48 | * @param permissions 需要请求的权限 49 | * @param callback 结果回调 50 | */ 51 | @TargetApi(Build.VERSION_CODES.M) 52 | private static void requestPermissions(Object object, int requestCode 53 | , String[] permissions, OnPermissionListener callback) { 54 | 55 | checkCallingObjectSuitability(object); 56 | mOnPermissionListener = callback; 57 | 58 | if (checkPermissions(getContext(object), permissions)) { 59 | if (mOnPermissionListener != null) 60 | mOnPermissionListener.onPermissionGranted(); 61 | } else { 62 | List deniedPermissions = getDeniedPermissions(getContext(object), permissions); 63 | if (deniedPermissions.size() > 0) { 64 | mRequestCode = requestCode; 65 | if (object instanceof Activity) { 66 | ((Activity) object).requestPermissions(deniedPermissions 67 | .toArray(new String[deniedPermissions.size()]), requestCode); 68 | } else if (object instanceof android.app.Fragment) { 69 | ((android.app.Fragment) object).requestPermissions(deniedPermissions 70 | .toArray(new String[deniedPermissions.size()]), requestCode); 71 | } else if (object instanceof android.support.v4.app.Fragment) { 72 | ((android.support.v4.app.Fragment) object).requestPermissions(deniedPermissions 73 | .toArray(new String[deniedPermissions.size()]), requestCode); 74 | } else { 75 | mRequestCode = -1; 76 | } 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * 获取上下文 83 | */ 84 | private static Context getContext(Object object) { 85 | Context context; 86 | if (object instanceof android.app.Fragment) { 87 | context = ((android.app.Fragment) object).getActivity(); 88 | } else if (object instanceof android.support.v4.app.Fragment) { 89 | context = ((android.support.v4.app.Fragment) object).getActivity(); 90 | } else { 91 | context = (Activity) object; 92 | } 93 | return context; 94 | } 95 | 96 | /** 97 | * 请求权限结果,对应onRequestPermissionsResult()方法。 98 | */ 99 | public static void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { 100 | if (mRequestCode != -1 && requestCode == mRequestCode) { 101 | if (verifyPermissions(grantResults)) { 102 | if (mOnPermissionListener != null) 103 | mOnPermissionListener.onPermissionGranted(); 104 | } else { 105 | if (mOnPermissionListener != null) 106 | mOnPermissionListener.onPermissionDenied(); 107 | } 108 | } 109 | } 110 | 111 | /** 112 | * 显示提示对话框 113 | */ 114 | public static void showTipsDialog(final Context context) { 115 | new AlertDialog.Builder(context) 116 | .setTitle("提示信息") 117 | .setMessage("当前应用缺少必要权限,该功能暂时无法使用。如若需要,请单击【确定】按钮前往设置中心进行权限授权。") 118 | .setNegativeButton("取消", null) 119 | .setPositiveButton("确定", new DialogInterface.OnClickListener() { 120 | @Override 121 | public void onClick(DialogInterface dialog, int which) { 122 | startAppSettings(context); 123 | } 124 | }).show(); 125 | } 126 | 127 | /** 128 | * 启动当前应用设置页面 129 | */ 130 | private static void startAppSettings(Context context) { 131 | Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 132 | intent.setData(Uri.parse("package:" + context.getPackageName())); 133 | context.startActivity(intent); 134 | } 135 | 136 | /** 137 | * 验证权限是否都已经授权 138 | */ 139 | private static boolean verifyPermissions(int[] grantResults) { 140 | for (int grantResult : grantResults) { 141 | if (grantResult != PackageManager.PERMISSION_GRANTED) { 142 | return false; 143 | } 144 | } 145 | return true; 146 | } 147 | 148 | /** 149 | * 获取权限列表中所有需要授权的权限 150 | * 151 | * @param context 上下文 152 | * @param permissions 权限列表 153 | * @return 154 | */ 155 | private static List getDeniedPermissions(Context context, String... permissions) { 156 | List deniedPermissions = new ArrayList<>(); 157 | for (String permission : permissions) { 158 | if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_DENIED) { 159 | deniedPermissions.add(permission); 160 | } 161 | } 162 | return deniedPermissions; 163 | } 164 | 165 | /** 166 | * 检查所传递对象的正确性 167 | * 168 | * @param object 必须为 activity or fragment 169 | */ 170 | private static void checkCallingObjectSuitability(Object object) { 171 | if (object == null) { 172 | throw new NullPointerException("Activity or Fragment should not be null"); 173 | } 174 | 175 | boolean isActivity = object instanceof Activity; 176 | boolean isSupportFragment = object instanceof android.support.v4.app.Fragment; 177 | boolean isAppFragment = object instanceof android.app.Fragment; 178 | 179 | if (!(isActivity || isSupportFragment || isAppFragment)) { 180 | throw new IllegalArgumentException( 181 | "Caller must be an Activity or a Fragment"); 182 | } 183 | } 184 | 185 | /** 186 | * 检查所有的权限是否已经被授权 187 | * 188 | * @param permissions 权限列表 189 | * @return 190 | */ 191 | private static boolean checkPermissions(Context context, String... permissions) { 192 | if (isOverMarshmallow()) { 193 | for (String permission : permissions) { 194 | if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_DENIED) { 195 | return false; 196 | } 197 | } 198 | } 199 | return true; 200 | } 201 | 202 | /** 203 | * 判断当前手机API版本是否 >= 6.0 204 | */ 205 | private static boolean isOverMarshmallow() { 206 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; 207 | } 208 | 209 | public interface OnPermissionListener { 210 | void onPermissionGranted(); 211 | 212 | void onPermissionDenied(); 213 | } 214 | 215 | private static OnPermissionListener mOnPermissionListener; 216 | 217 | } 218 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/utils/OtherUtils.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.utils; 2 | 3 | import android.content.Context; 4 | import android.content.pm.PackageInfo; 5 | import android.content.pm.PackageManager; 6 | import android.os.Environment; 7 | import android.text.TextUtils; 8 | 9 | import java.io.File; 10 | import java.security.MessageDigest; 11 | import java.security.NoSuchAlgorithmException; 12 | import java.util.Date; 13 | 14 | /** 15 | * Description:屏幕、尺寸转换工具类 16 | *

17 | * Created by Mjj on 2016/12/2. 18 | */ 19 | 20 | public class OtherUtils { 21 | 22 | /** 23 | * 判断外部存储卡是否可用 24 | * 25 | * @return 26 | */ 27 | public static boolean isExternalStorageAvailable() { 28 | if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { 29 | return true; 30 | } 31 | return false; 32 | } 33 | 34 | public static final int getHeightInPx(Context context) { 35 | final int height = context.getResources().getDisplayMetrics().heightPixels; 36 | return height; 37 | } 38 | 39 | public static final int getWidthInPx(Context context) { 40 | final int width = context.getResources().getDisplayMetrics().widthPixels; 41 | return width; 42 | } 43 | 44 | public static final int getHeightInDp(Context context) { 45 | final float height = context.getResources().getDisplayMetrics().heightPixels; 46 | int heightInDp = px2dip(context, height); 47 | return heightInDp; 48 | } 49 | 50 | public static final int getWidthInDp(Context context) { 51 | final float width = context.getResources().getDisplayMetrics().widthPixels; 52 | int widthInDp = px2dip(context, width); 53 | return widthInDp; 54 | } 55 | 56 | public static int dip2px(Context context, float dpValue) { 57 | final float scale = context.getResources().getDisplayMetrics().density; 58 | return (int) (dpValue * scale + 0.5f); 59 | } 60 | 61 | public static int px2dip(Context context, float pxValue) { 62 | final float scale = context.getResources().getDisplayMetrics().density; 63 | return (int) (pxValue / scale + 0.5f); 64 | } 65 | 66 | /** 67 | * 根据string.xml资源格式化字符串 68 | * 69 | * @param context 70 | * @param resource 71 | * @param args 72 | * @return 73 | */ 74 | public static String formatResourceString(Context context, int resource, Object... args) { 75 | String str = context.getResources().getString(resource); 76 | if (TextUtils.isEmpty(str)) { 77 | return null; 78 | } 79 | return String.format(str, args); 80 | } 81 | 82 | /** 83 | * 获取拍照相片存储文件 84 | * 85 | * @param context 86 | * @return 87 | */ 88 | public static File createFile(Context context) { 89 | File file; 90 | if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { 91 | String timeStamp = String.valueOf(new Date().getTime()); 92 | file = new File(Environment.getExternalStorageDirectory() + 93 | File.separator + timeStamp + ".jpg"); 94 | } else { 95 | File cacheDir = context.getCacheDir(); 96 | String timeStamp = String.valueOf(new Date().getTime()); 97 | file = new File(cacheDir, timeStamp + ".jpg"); 98 | } 99 | return file; 100 | } 101 | 102 | /** 103 | * 获取磁盘缓存文件 104 | * 105 | * @param context 106 | * @param uniqueName 107 | * @return 108 | */ 109 | public static File getDiskCacheDir(Context context, String uniqueName) { 110 | String cachePath; 111 | if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) 112 | || !Environment.isExternalStorageRemovable()) { 113 | cachePath = context.getExternalCacheDir().getPath(); 114 | } else { 115 | cachePath = context.getCacheDir().getPath(); 116 | } 117 | return new File(cachePath + File.separator + uniqueName); 118 | } 119 | 120 | /** 121 | * 获取应用程序版本号 122 | * 123 | * @param context 124 | * @return 125 | */ 126 | public static int getAppVersion(Context context) { 127 | try { 128 | PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); 129 | return info.versionCode; 130 | } catch (PackageManager.NameNotFoundException e) { 131 | e.printStackTrace(); 132 | } 133 | return 1; 134 | } 135 | 136 | public static String hashKeyForDisk(String key) { 137 | String cacheKey; 138 | try { 139 | final MessageDigest mDigest = MessageDigest.getInstance("MD5"); 140 | mDigest.update(key.getBytes()); 141 | cacheKey = bytesToHexString(mDigest.digest()); 142 | } catch (NoSuchAlgorithmException e) { 143 | cacheKey = String.valueOf(key.hashCode()); 144 | } 145 | return cacheKey; 146 | } 147 | 148 | private static String bytesToHexString(byte[] bytes) { 149 | StringBuilder sb = new StringBuilder(); 150 | for (int i = 0; i < bytes.length; i++) { 151 | String hex = Integer.toHexString(0xFF & bytes[i]); 152 | if (hex.length() == 1) { 153 | sb.append('0'); 154 | } 155 | sb.append(hex); 156 | } 157 | return sb.toString(); 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/utils/PhotoUtils.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.utils; 2 | 3 | import android.content.ContentResolver; 4 | import android.content.Context; 5 | import android.database.Cursor; 6 | import android.net.Uri; 7 | import android.provider.MediaStore; 8 | 9 | import com.example.mjj.selectphotodemo.beans.Photo; 10 | import com.example.mjj.selectphotodemo.beans.PhotoFolder; 11 | 12 | import java.io.File; 13 | import java.util.ArrayList; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | /** 19 | * Description:异步获取本地图片工具类 20 | *

21 | * Created by Mjj on 2016/12/2. 22 | */ 23 | 24 | public class PhotoUtils { 25 | 26 | public static Map getPhotos(Context context) { 27 | Map folderMap = new HashMap<>(); 28 | 29 | String allPhotosKey = "所有图片"; 30 | PhotoFolder allFolder = new PhotoFolder(); 31 | allFolder.setName(allPhotosKey); 32 | allFolder.setDirPath(allPhotosKey); 33 | allFolder.setPhotoList(new ArrayList()); 34 | folderMap.put(allPhotosKey, allFolder); 35 | 36 | Uri imageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; 37 | ContentResolver mContentResolver = context.getContentResolver(); 38 | 39 | // 只查询jpeg和png的图片 40 | Cursor mCursor = mContentResolver.query(imageUri, null, 41 | MediaStore.Images.Media.MIME_TYPE + " in(?, ?)", 42 | new String[]{"image/jpeg", "image/png"}, 43 | MediaStore.Images.Media.DATE_MODIFIED + " desc"); 44 | 45 | int pathIndex = mCursor 46 | .getColumnIndex(MediaStore.Images.Media.DATA); 47 | 48 | if (mCursor.moveToFirst()) { 49 | do { 50 | // 获取图片的路径 51 | String path = mCursor.getString(pathIndex); 52 | // 获取该图片的父路径名 53 | File parentFile = new File(path).getParentFile(); 54 | if (parentFile == null) { 55 | continue; 56 | } 57 | String dirPath = parentFile.getAbsolutePath(); 58 | 59 | if (folderMap.containsKey(dirPath)) { 60 | Photo photo = new Photo(path); 61 | PhotoFolder photoFolder = folderMap.get(dirPath); 62 | photoFolder.getPhotoList().add(photo); 63 | folderMap.get(allPhotosKey).getPhotoList().add(photo); 64 | continue; 65 | } else { 66 | // 初始化imageFolder 67 | PhotoFolder photoFolder = new PhotoFolder(); 68 | List photoList = new ArrayList<>(); 69 | Photo photo = new Photo(path); 70 | photoList.add(photo); 71 | photoFolder.setPhotoList(photoList); 72 | photoFolder.setDirPath(dirPath); 73 | photoFolder.setName(dirPath.substring(dirPath.lastIndexOf(File.separator) + 1, dirPath.length())); 74 | folderMap.put(dirPath, photoFolder); 75 | folderMap.get(allPhotosKey).getPhotoList().add(photo); 76 | } 77 | } while (mCursor.moveToNext()); 78 | } 79 | mCursor.close(); 80 | return folderMap; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/widgets/SquareImageView.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.widgets; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.widget.ImageView; 6 | 7 | import com.example.mjj.selectphotodemo.utils.OtherUtils; 8 | 9 | /** 10 | * @Class: SquareImageView 11 | * @Description: 12 | * @author: lling(www.liuling123.com) 13 | * @Date: 2015/11/5 14 | */ 15 | public class SquareImageView extends ImageView { 16 | 17 | Context mContext; 18 | int mWidth; 19 | 20 | public SquareImageView(Context context) { 21 | this(context, null); 22 | } 23 | 24 | public SquareImageView(Context context, AttributeSet attrs) { 25 | this(context, attrs, 0); 26 | } 27 | 28 | public SquareImageView(Context context, AttributeSet attrs, int defStyleAttr) { 29 | super(context, attrs, defStyleAttr); 30 | this.mContext = context; 31 | int screenWidth = OtherUtils.getWidthInPx(mContext); 32 | mWidth = (screenWidth - OtherUtils.dip2px(mContext, 4))/3; 33 | } 34 | 35 | @Override 36 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 37 | setMeasuredDimension(mWidth, mWidth); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/zoom/Compat.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.zoom; 2 | 3 | import android.os.Build.VERSION; 4 | import android.os.Build.VERSION_CODES; 5 | import android.view.View; 6 | 7 | public class Compat { 8 | 9 | private static final int SIXTY_FPS_INTERVAL = 1000 / 60; 10 | 11 | public static void postOnAnimation(View view, Runnable runnable) { 12 | if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { 13 | SDK16.postOnAnimation(view, runnable); 14 | } else { 15 | view.postDelayed(runnable, SIXTY_FPS_INTERVAL); 16 | } 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/zoom/IPhotoView.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.zoom; 2 | 3 | import android.graphics.RectF; 4 | import android.view.View; 5 | import android.widget.ImageView; 6 | 7 | 8 | public interface IPhotoView { 9 | /** 10 | * Returns true if the PhotoView is set to allow zooming of Photos. 11 | * 12 | * @return true if the PhotoView allows zooming. 13 | */ 14 | boolean canZoom(); 15 | 16 | /** 17 | * Gets the Display Rectangle of the currently displayed Drawable. The 18 | * Rectangle is relative to this View and includes all scaling and 19 | * translations. 20 | * 21 | * @return - RectF of Displayed Drawable 22 | */ 23 | RectF getDisplayRect(); 24 | 25 | /** 26 | * @return The current minimum scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}. 27 | */ 28 | float getMinScale(); 29 | 30 | /** 31 | * @return The current middle scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}. 32 | */ 33 | float getMidScale(); 34 | 35 | /** 36 | * @return The current maximum scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}. 37 | */ 38 | float getMaxScale(); 39 | 40 | /** 41 | * Returns the current scale value 42 | * 43 | * @return float - current scale value 44 | */ 45 | float getScale(); 46 | 47 | /** 48 | * Return the current scale type in use by the ImageView. 49 | */ 50 | ImageView.ScaleType getScaleType(); 51 | 52 | /** 53 | * Whether to allow the ImageView's parent to intercept the touch event when the photo is scroll to it's horizontal edge. 54 | */ 55 | void setAllowParentInterceptOnEdge(boolean allow); 56 | 57 | /** 58 | * Sets the minimum scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}. 59 | */ 60 | void setMinScale(float minScale); 61 | 62 | /** 63 | * Sets the middle scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}. 64 | */ 65 | void setMidScale(float midScale); 66 | 67 | /** 68 | * Sets the maximum scale level. What this value represents depends on the current {@link android.widget.ImageView.ScaleType}. 69 | */ 70 | void setMaxScale(float maxScale); 71 | 72 | /** 73 | * Register a callback to be invoked when the Photo displayed by this view is long-pressed. 74 | * 75 | * @param listener - Listener to be registered. 76 | */ 77 | void setOnLongClickListener(View.OnLongClickListener listener); 78 | 79 | /** 80 | * Register a callback to be invoked when the Matrix has changed for this 81 | * View. An example would be the user panning or scaling the Photo. 82 | * 83 | * @param listener - Listener to be registered. 84 | */ 85 | void setOnMatrixChangeListener(PhotoViewAttacher.OnMatrixChangedListener listener); 86 | 87 | /** 88 | * Register a callback to be invoked when the Photo displayed by this View 89 | * is tapped with a single tap. 90 | * 91 | * @param listener - Listener to be registered. 92 | */ 93 | void setOnPhotoTapListener(PhotoViewAttacher.OnPhotoTapListener listener); 94 | 95 | /** 96 | * Register a callback to be invoked when the View is tapped with a single 97 | * tap. 98 | * 99 | * @param listener - Listener to be registered. 100 | */ 101 | void setOnViewTapListener(PhotoViewAttacher.OnViewTapListener listener); 102 | 103 | /** 104 | * Controls how the image should be resized or moved to match the size of 105 | * the ImageView. Any scaling or panning will happen within the confines of 106 | * this {@link android.widget.ImageView.ScaleType}. 107 | * 108 | * @param scaleType - The desired scaling mode. 109 | */ 110 | void setScaleType(ImageView.ScaleType scaleType); 111 | 112 | /** 113 | * Allows you to enable/disable the zoom functionality on the ImageView. 114 | * When disable the ImageView reverts to using the FIT_CENTER matrix. 115 | * 116 | * @param zoomable - Whether the zoom functionality is enabled. 117 | */ 118 | void setZoomable(boolean zoomable); 119 | 120 | /** 121 | * Zooms to the specified scale, around the focal point given. 122 | * 123 | * @param scale - Scale to zoom to 124 | * @param focalX - X Focus Point 125 | * @param focalY - Y Focus Point 126 | */ 127 | void zoomTo(float scale, float focalX, float focalY); 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/zoom/PhotoView.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.zoom; 2 | 3 | import android.content.Context; 4 | import android.graphics.RectF; 5 | import android.graphics.drawable.Drawable; 6 | import android.net.Uri; 7 | import android.util.AttributeSet; 8 | import android.widget.ImageView; 9 | 10 | public class PhotoView extends ImageView implements IPhotoView { 11 | 12 | private final PhotoViewAttacher mAttacher; 13 | 14 | private ScaleType mPendingScaleType; 15 | 16 | public PhotoView(Context context) { 17 | this(context, null); 18 | } 19 | 20 | public PhotoView(Context context, AttributeSet attr) { 21 | this(context, attr, 0); 22 | } 23 | 24 | public PhotoView(Context context, AttributeSet attr, int defStyle) { 25 | super(context, attr, defStyle); 26 | // 设置为矩阵即可有双击放大效果 27 | // super.setScaleType(ScaleType.MATRIX); 28 | super.setScaleType(ScaleType.CENTER_CROP); 29 | mAttacher = new PhotoViewAttacher(this); 30 | 31 | if (null != mPendingScaleType) { 32 | setScaleType(mPendingScaleType); 33 | mPendingScaleType = null; 34 | } 35 | } 36 | 37 | @Override 38 | public boolean canZoom() { 39 | return mAttacher.canZoom(); 40 | } 41 | 42 | @Override 43 | public RectF getDisplayRect() { 44 | return mAttacher.getDisplayRect(); 45 | } 46 | 47 | @Override 48 | public float getMinScale() { 49 | return mAttacher.getMinScale(); 50 | } 51 | 52 | @Override 53 | public float getMidScale() { 54 | return mAttacher.getMidScale(); 55 | } 56 | 57 | @Override 58 | public float getMaxScale() { 59 | return mAttacher.getMaxScale(); 60 | } 61 | 62 | @Override 63 | public float getScale() { 64 | return mAttacher.getScale(); 65 | } 66 | 67 | @Override 68 | public ScaleType getScaleType() { 69 | return mAttacher.getScaleType(); 70 | } 71 | 72 | @Override 73 | public void setAllowParentInterceptOnEdge(boolean allow) { 74 | mAttacher.setAllowParentInterceptOnEdge(allow); 75 | } 76 | 77 | @Override 78 | public void setMinScale(float minScale) { 79 | mAttacher.setMinScale(minScale); 80 | } 81 | 82 | @Override 83 | public void setMidScale(float midScale) { 84 | mAttacher.setMidScale(midScale); 85 | } 86 | 87 | @Override 88 | public void setMaxScale(float maxScale) { 89 | mAttacher.setMaxScale(maxScale); 90 | } 91 | 92 | @Override 93 | // setImageBitmap calls through to this method 94 | public void setImageDrawable(Drawable drawable) { 95 | super.setImageDrawable(drawable); 96 | if (null != mAttacher) { 97 | mAttacher.update(); 98 | } 99 | } 100 | 101 | @Override 102 | public void setImageResource(int resId) { 103 | super.setImageResource(resId); 104 | if (null != mAttacher) { 105 | mAttacher.update(); 106 | } 107 | } 108 | 109 | @Override 110 | public void setImageURI(Uri uri) { 111 | super.setImageURI(uri); 112 | if (null != mAttacher) { 113 | mAttacher.update(); 114 | } 115 | } 116 | 117 | @Override 118 | public void setOnMatrixChangeListener(PhotoViewAttacher.OnMatrixChangedListener listener) { 119 | mAttacher.setOnMatrixChangeListener(listener); 120 | } 121 | 122 | @Override 123 | public void setOnLongClickListener(OnLongClickListener l) { 124 | mAttacher.setOnLongClickListener(l); 125 | } 126 | 127 | @Override 128 | public void setOnPhotoTapListener(PhotoViewAttacher.OnPhotoTapListener listener) { 129 | mAttacher.setOnPhotoTapListener(listener); 130 | } 131 | 132 | @Override 133 | public void setOnViewTapListener(PhotoViewAttacher.OnViewTapListener listener) { 134 | mAttacher.setOnViewTapListener(listener); 135 | } 136 | 137 | @Override 138 | public void setScaleType(ScaleType scaleType) { 139 | if (null != mAttacher) { 140 | mAttacher.setScaleType(scaleType); 141 | } else { 142 | mPendingScaleType = scaleType; 143 | } 144 | } 145 | 146 | @Override 147 | public void setZoomable(boolean zoomable) { 148 | mAttacher.setZoomable(zoomable); 149 | } 150 | 151 | @Override 152 | public void zoomTo(float scale, float focalX, float focalY) { 153 | mAttacher.zoomTo(scale, focalX, focalY); 154 | } 155 | 156 | @Override 157 | protected void onDetachedFromWindow() { 158 | mAttacher.cleanup(); 159 | super.onDetachedFromWindow(); 160 | } 161 | 162 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/zoom/PhotoViewAttacher.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.zoom; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.graphics.Matrix; 6 | import android.graphics.Matrix.ScaleToFit; 7 | import android.graphics.RectF; 8 | import android.graphics.drawable.Drawable; 9 | import android.os.Build.VERSION; 10 | import android.os.Build.VERSION_CODES; 11 | import android.util.Log; 12 | import android.view.GestureDetector; 13 | import android.view.MotionEvent; 14 | import android.view.View; 15 | import android.view.View.OnLongClickListener; 16 | import android.view.ViewTreeObserver; 17 | import android.widget.ImageView; 18 | import android.widget.ImageView.ScaleType; 19 | 20 | import java.lang.ref.WeakReference; 21 | 22 | public class PhotoViewAttacher implements IPhotoView, View.OnTouchListener, 23 | VersionedGestureDetector.OnGestureListener, 24 | GestureDetector.OnDoubleTapListener, 25 | ViewTreeObserver.OnGlobalLayoutListener { 26 | 27 | static final String LOG_TAG = "PhotoViewAttacher"; 28 | 29 | // let debug flag be dynamic, but still Proguard can be used to remove from 30 | // release builds 31 | static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG); 32 | 33 | static final int EDGE_NONE = -1; 34 | static final int EDGE_LEFT = 0; 35 | static final int EDGE_RIGHT = 1; 36 | static final int EDGE_BOTH = 2; 37 | 38 | public static final float DEFAULT_MAX_SCALE = 3.0f; 39 | public static final float DEFAULT_MID_SCALE = 1.75f; 40 | public static final float DEFAULT_MIN_SCALE = 1.0f; 41 | 42 | private float mMinScale = DEFAULT_MIN_SCALE; 43 | private float mMidScale = DEFAULT_MID_SCALE; 44 | private float mMaxScale = DEFAULT_MAX_SCALE; 45 | 46 | private boolean mAllowParentInterceptOnEdge = true; 47 | 48 | private static void checkZoomLevels(float minZoom, float midZoom, 49 | float maxZoom) { 50 | if (minZoom >= midZoom) { 51 | throw new IllegalArgumentException( 52 | "MinZoom should be less than MidZoom"); 53 | } else if (midZoom >= maxZoom) { 54 | throw new IllegalArgumentException( 55 | "MidZoom should be less than MaxZoom"); 56 | } 57 | } 58 | 59 | /** 60 | * @return true if the ImageView exists, and it's Drawable existss 61 | */ 62 | private static boolean hasDrawable(ImageView imageView) { 63 | return null != imageView && null != imageView.getDrawable(); 64 | } 65 | 66 | /** 67 | * @return true if the ScaleType is supported. 68 | */ 69 | private static boolean isSupportedScaleType(final ScaleType scaleType) { 70 | if (null == scaleType) { 71 | return false; 72 | } 73 | 74 | switch (scaleType) { 75 | case MATRIX: 76 | throw new IllegalArgumentException(scaleType.name() 77 | + " is not supported in PhotoView"); 78 | 79 | default: 80 | return true; 81 | } 82 | } 83 | 84 | /** 85 | * Set's the ImageView's ScaleType to Matrix. 86 | */ 87 | private static void setImageViewScaleTypeMatrix(ImageView imageView) { 88 | if (null != imageView) { 89 | if (imageView instanceof PhotoView) { 90 | /** 91 | * PhotoView sets it's own ScaleType to Matrix, then diverts all 92 | * calls setScaleType to this.setScaleType. Basically we don't 93 | * need to do anything here 94 | */ 95 | } else { 96 | imageView.setScaleType(ScaleType.MATRIX); 97 | } 98 | } 99 | } 100 | 101 | private WeakReference mImageView; 102 | private ViewTreeObserver mViewTreeObserver; 103 | 104 | // Gesture Detectors 105 | private GestureDetector mGestureDetector; 106 | private VersionedGestureDetector mScaleDragDetector; 107 | 108 | // These are set so we don't keep allocating them on the heap 109 | private final Matrix mBaseMatrix = new Matrix(); 110 | private final Matrix mDrawMatrix = new Matrix(); 111 | private final Matrix mSuppMatrix = new Matrix(); 112 | private final RectF mDisplayRect = new RectF(); 113 | private final float[] mMatrixValues = new float[9]; 114 | 115 | // Listeners 116 | private OnMatrixChangedListener mMatrixChangeListener; 117 | private OnPhotoTapListener mPhotoTapListener; 118 | private OnViewTapListener mViewTapListener; 119 | private OnLongClickListener mLongClickListener; 120 | 121 | private int mIvTop, mIvRight, mIvBottom, mIvLeft; 122 | private FlingRunnable mCurrentFlingRunnable; 123 | private int mScrollEdge = EDGE_BOTH; 124 | 125 | private boolean mZoomEnabled; 126 | private ScaleType mScaleType = ScaleType.FIT_CENTER; 127 | 128 | public PhotoViewAttacher(ImageView imageView) { 129 | mImageView = new WeakReference(imageView); 130 | 131 | imageView.setOnTouchListener(this); 132 | 133 | mViewTreeObserver = imageView.getViewTreeObserver(); 134 | mViewTreeObserver.addOnGlobalLayoutListener(this); 135 | 136 | // Make sure we using MATRIX Scale Type 137 | setImageViewScaleTypeMatrix(imageView); 138 | 139 | if (!imageView.isInEditMode()) { 140 | // Create Gesture Detectors... 141 | mScaleDragDetector = VersionedGestureDetector.newInstance( 142 | imageView.getContext(), this); 143 | 144 | mGestureDetector = new GestureDetector(imageView.getContext(), 145 | new GestureDetector.SimpleOnGestureListener() { 146 | 147 | // forward long click listener 148 | @Override 149 | public void onLongPress(MotionEvent e) { 150 | if (null != mLongClickListener) { 151 | mLongClickListener.onLongClick(mImageView.get()); 152 | } 153 | } 154 | }); 155 | 156 | mGestureDetector.setOnDoubleTapListener(this); 157 | 158 | // Finally, update the UI so that we're zoomable 159 | setZoomable(true); 160 | } 161 | } 162 | 163 | @Override 164 | public final boolean canZoom() { 165 | return mZoomEnabled; 166 | } 167 | 168 | /** 169 | * Clean-up the resources attached to this object. This needs to be called 170 | * when the ImageView is no longer used. A good example is from 171 | * {@link android.view.View#onDetachedFromWindow()} or from 172 | * {@link android.app.Activity#onDestroy()}. This is automatically called if 173 | * you are using {@link uk.co.senab.photoview.PhotoView}. 174 | */ 175 | @SuppressLint("NewApi") 176 | // @SuppressWarnings("deprecation") 177 | // public final void cleanup() { 178 | // if (null != mImageView) { 179 | // mImageView.get().getViewTreeObserver().removeGlobalOnLayoutListener(this); 180 | // } 181 | // mViewTreeObserver = null; 182 | // 183 | // // Clear listeners too 184 | // mMatrixChangeListener = null; 185 | // mPhotoTapListener = null; 186 | // mViewTapListener = null; 187 | // 188 | // // Finally, clear ImageView 189 | // mImageView = null; 190 | // } 191 | @SuppressWarnings("deprecation") 192 | public final void cleanup() { 193 | if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { 194 | if (null != mImageView) { 195 | mImageView.get().getViewTreeObserver() 196 | .removeOnGlobalLayoutListener(this); 197 | } 198 | 199 | if (null != mViewTreeObserver && mViewTreeObserver.isAlive()) { 200 | mViewTreeObserver.removeOnGlobalLayoutListener(this); 201 | 202 | mViewTreeObserver = null; 203 | 204 | // Clear listeners too 205 | mMatrixChangeListener = null; 206 | mPhotoTapListener = null; 207 | mViewTapListener = null; 208 | // Finally, clear ImageView 209 | mImageView = null; 210 | } 211 | 212 | } else { 213 | if (null != mImageView) { 214 | mImageView.get().getViewTreeObserver() 215 | .removeGlobalOnLayoutListener(this); 216 | } 217 | 218 | if (null != mViewTreeObserver && mViewTreeObserver.isAlive()) { 219 | mViewTreeObserver.removeGlobalOnLayoutListener(this); 220 | 221 | mViewTreeObserver = null; 222 | 223 | // Clear listeners too 224 | mMatrixChangeListener = null; 225 | mPhotoTapListener = null; 226 | mViewTapListener = null; 227 | // Finally, clear ImageView 228 | mImageView = null; 229 | } 230 | } 231 | } 232 | 233 | @Override 234 | public final RectF getDisplayRect() { 235 | checkMatrixBounds(); 236 | return getDisplayRect(getDisplayMatrix()); 237 | } 238 | 239 | public final ImageView getImageView() { 240 | ImageView imageView = null; 241 | 242 | if (null != mImageView) { 243 | imageView = mImageView.get(); 244 | } 245 | 246 | // If we don't have an ImageView, call cleanup() 247 | if (null == imageView) { 248 | cleanup(); 249 | throw new IllegalStateException( 250 | "ImageView no longer exists. You should not use this PhotoViewAttacher any more."); 251 | } 252 | 253 | return imageView; 254 | } 255 | 256 | @Override 257 | public float getMinScale() { 258 | return mMinScale; 259 | } 260 | 261 | @Override 262 | public float getMidScale() { 263 | return mMidScale; 264 | } 265 | 266 | @Override 267 | public float getMaxScale() { 268 | return mMaxScale; 269 | } 270 | 271 | @Override 272 | public final float getScale() { 273 | return getValue(mSuppMatrix, Matrix.MSCALE_X); 274 | } 275 | 276 | @Override 277 | public final ScaleType getScaleType() { 278 | return mScaleType; 279 | } 280 | 281 | public final boolean onDoubleTap(MotionEvent ev) { 282 | try { 283 | float scale = getScale(); 284 | float x = ev.getX(); 285 | float y = ev.getY(); 286 | 287 | if (scale < mMidScale) { 288 | zoomTo(mMidScale, x, y); 289 | } else if (scale >= mMidScale && scale < mMaxScale) { 290 | zoomTo(mMaxScale, x, y); 291 | } else { 292 | zoomTo(mMinScale, x, y); 293 | } 294 | } catch (ArrayIndexOutOfBoundsException e) { 295 | // Can sometimes happen when getX() and getY() is called 296 | } 297 | 298 | return true; 299 | } 300 | 301 | public final boolean onDoubleTapEvent(MotionEvent e) { 302 | // Wait for the confirmed onDoubleTap() instead 303 | return false; 304 | } 305 | 306 | public final void onDrag(float dx, float dy) { 307 | if (DEBUG) { 308 | Log.d(LOG_TAG, String.format("onDrag: dx: %.2f. dy: %.2f", dx, dy)); 309 | } 310 | 311 | ImageView imageView = getImageView(); 312 | 313 | if (null != imageView && hasDrawable(imageView)) { 314 | mSuppMatrix.postTranslate(dx, dy); 315 | checkAndDisplayMatrix(); 316 | 317 | /** 318 | * Here we decide whether to let the ImageView's parent to start 319 | * taking over the touch event. 320 | * 321 | * First we check whether this function is enabled. We never want 322 | * the parent to take over if we're scaling. We then check the edge 323 | * we're on, and the direction of the scroll (i.e. if we're pulling 324 | * against the edge, aka 'overscrolling', let the parent take over). 325 | */ 326 | if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling()) { 327 | if (mScrollEdge == EDGE_BOTH 328 | || (mScrollEdge == EDGE_LEFT && dx >= 1f) 329 | || (mScrollEdge == EDGE_RIGHT && dx <= -1f)) { 330 | imageView.getParent().requestDisallowInterceptTouchEvent( 331 | false); 332 | } 333 | } 334 | } 335 | } 336 | 337 | @Override 338 | public final void onFling(float startX, float startY, float velocityX, 339 | float velocityY) { 340 | if (DEBUG) { 341 | Log.d(LOG_TAG, "onFling. sX: " + startX + " sY: " + startY 342 | + " Vx: " + velocityX + " Vy: " + velocityY); 343 | } 344 | 345 | ImageView imageView = getImageView(); 346 | if (hasDrawable(imageView)) { 347 | mCurrentFlingRunnable = new FlingRunnable(imageView.getContext()); 348 | mCurrentFlingRunnable.fling(imageView.getWidth(), 349 | imageView.getHeight(), (int) velocityX, (int) velocityY); 350 | imageView.post(mCurrentFlingRunnable); 351 | } 352 | } 353 | 354 | @Override 355 | public final void onGlobalLayout() { 356 | ImageView imageView = getImageView(); 357 | 358 | if (null != imageView && mZoomEnabled) { 359 | final int top = imageView.getTop(); 360 | final int right = imageView.getRight(); 361 | final int bottom = imageView.getBottom(); 362 | final int left = imageView.getLeft(); 363 | 364 | /** 365 | * We need to check whether the ImageView's bounds have changed. 366 | * This would be easier if we targeted API 11+ as we could just use 367 | * View.OnLayoutChangeListener. Instead we have to replicate the 368 | * work, keeping track of the ImageView's bounds and then checking 369 | * if the values change. 370 | */ 371 | if (top != mIvTop || bottom != mIvBottom || left != mIvLeft 372 | || right != mIvRight) { 373 | // Update our base matrix, as the bounds have changed 374 | updateBaseMatrix(imageView.getDrawable()); 375 | 376 | // Update values as something has changed 377 | mIvTop = top; 378 | mIvRight = right; 379 | mIvBottom = bottom; 380 | mIvLeft = left; 381 | } 382 | } 383 | } 384 | 385 | public final void onScale(float scaleFactor, float focusX, float focusY) { 386 | if (DEBUG) { 387 | Log.d(LOG_TAG, String.format( 388 | "onScale: scale: %.2f. fX: %.2f. fY: %.2f", scaleFactor, 389 | focusX, focusY)); 390 | } 391 | 392 | if (hasDrawable(getImageView()) 393 | && (getScale() < mMaxScale || scaleFactor < 1f)) { 394 | mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY); 395 | checkAndDisplayMatrix(); 396 | } 397 | } 398 | 399 | public final boolean onSingleTapConfirmed(MotionEvent e) { 400 | ImageView imageView = getImageView(); 401 | 402 | if (null != imageView) { 403 | if (null != mPhotoTapListener) { 404 | final RectF displayRect = getDisplayRect(); 405 | 406 | if (null != displayRect) { 407 | final float x = e.getX(), y = e.getY(); 408 | 409 | // Check to see if the user tapped on the photo 410 | if (displayRect.contains(x, y)) { 411 | 412 | float xResult = (x - displayRect.left) 413 | / displayRect.width(); 414 | float yResult = (y - displayRect.top) 415 | / displayRect.height(); 416 | 417 | mPhotoTapListener.onPhotoTap(imageView, xResult, 418 | yResult); 419 | return true; 420 | } 421 | } 422 | } 423 | if (null != mViewTapListener) { 424 | mViewTapListener.onViewTap(imageView, e.getX(), e.getY()); 425 | } 426 | } 427 | 428 | return false; 429 | } 430 | 431 | @Override 432 | public final boolean onTouch(View v, MotionEvent ev) { 433 | boolean handled = false; 434 | 435 | if (mZoomEnabled) { 436 | switch (ev.getAction()) { 437 | case MotionEvent.ACTION_DOWN: 438 | // First, disable the Parent from intercepting the touch 439 | // event 440 | v.getParent().requestDisallowInterceptTouchEvent(true); 441 | 442 | // If we're flinging, and the user presses down, cancel 443 | // fling 444 | cancelFling(); 445 | break; 446 | 447 | case MotionEvent.ACTION_CANCEL: 448 | case MotionEvent.ACTION_UP: 449 | // If the user has zoomed less than min scale, zoom back 450 | // to min scale 451 | if (getScale() < mMinScale) { 452 | RectF rect = getDisplayRect(); 453 | if (null != rect) { 454 | v.post(new AnimatedZoomRunnable(getScale(), mMinScale, 455 | rect.centerX(), rect.centerY())); 456 | handled = true; 457 | } 458 | } 459 | break; 460 | } 461 | 462 | // Check to see if the user double tapped 463 | if (null != mGestureDetector && mGestureDetector.onTouchEvent(ev)) { 464 | handled = true; 465 | } 466 | 467 | // Finally, try the Scale/Drag detector 468 | if (null != mScaleDragDetector 469 | && mScaleDragDetector.onTouchEvent(ev)) { 470 | handled = true; 471 | } 472 | } 473 | 474 | return handled; 475 | } 476 | 477 | @Override 478 | public void setAllowParentInterceptOnEdge(boolean allow) { 479 | mAllowParentInterceptOnEdge = allow; 480 | } 481 | 482 | @Override 483 | public void setMinScale(float minScale) { 484 | checkZoomLevels(minScale, mMidScale, mMaxScale); 485 | mMinScale = minScale; 486 | } 487 | 488 | @Override 489 | public void setMidScale(float midScale) { 490 | checkZoomLevels(mMinScale, midScale, mMaxScale); 491 | mMidScale = midScale; 492 | } 493 | 494 | @Override 495 | public void setMaxScale(float maxScale) { 496 | checkZoomLevels(mMinScale, mMidScale, maxScale); 497 | mMaxScale = maxScale; 498 | } 499 | 500 | @Override 501 | public final void setOnLongClickListener(OnLongClickListener listener) { 502 | mLongClickListener = listener; 503 | } 504 | 505 | @Override 506 | public final void setOnMatrixChangeListener(OnMatrixChangedListener listener) { 507 | mMatrixChangeListener = listener; 508 | } 509 | 510 | @Override 511 | public final void setOnPhotoTapListener(OnPhotoTapListener listener) { 512 | mPhotoTapListener = listener; 513 | } 514 | 515 | @Override 516 | public final void setOnViewTapListener(OnViewTapListener listener) { 517 | mViewTapListener = listener; 518 | } 519 | 520 | @Override 521 | public final void setScaleType(ScaleType scaleType) { 522 | if (isSupportedScaleType(scaleType) && scaleType != mScaleType) { 523 | mScaleType = scaleType; 524 | 525 | // Finally update 526 | update(); 527 | } 528 | } 529 | 530 | @Override 531 | public final void setZoomable(boolean zoomable) { 532 | mZoomEnabled = zoomable; 533 | update(); 534 | } 535 | 536 | public final void update() { 537 | ImageView imageView = getImageView(); 538 | 539 | if (null != imageView) { 540 | if (mZoomEnabled) { 541 | // Make sure we using MATRIX Scale Type 542 | setImageViewScaleTypeMatrix(imageView); 543 | 544 | // Update the base matrix using the current drawable 545 | updateBaseMatrix(imageView.getDrawable()); 546 | } else { 547 | // Reset the Matrix... 548 | resetMatrix(); 549 | } 550 | } 551 | } 552 | 553 | @Override 554 | public final void zoomTo(float scale, float focalX, float focalY) { 555 | ImageView imageView = getImageView(); 556 | 557 | if (null != imageView) { 558 | imageView.post(new AnimatedZoomRunnable(getScale(), scale, focalX, 559 | focalY)); 560 | } 561 | } 562 | 563 | protected Matrix getDisplayMatrix() { 564 | mDrawMatrix.set(mBaseMatrix); 565 | mDrawMatrix.postConcat(mSuppMatrix); 566 | return mDrawMatrix; 567 | } 568 | 569 | private void cancelFling() { 570 | if (null != mCurrentFlingRunnable) { 571 | mCurrentFlingRunnable.cancelFling(); 572 | mCurrentFlingRunnable = null; 573 | } 574 | } 575 | 576 | /** 577 | * Helper method that simply checks the Matrix, and then displays the result 578 | */ 579 | private void checkAndDisplayMatrix() { 580 | checkMatrixBounds(); 581 | setImageViewMatrix(getDisplayMatrix()); 582 | } 583 | 584 | private void checkImageViewScaleType() { 585 | ImageView imageView = getImageView(); 586 | 587 | /** 588 | * PhotoView's getScaleType() will just divert to this.getScaleType() so 589 | * only call if we're not attached to a PhotoView. 590 | */ 591 | if (null != imageView && !(imageView instanceof PhotoView)) { 592 | if (imageView.getScaleType() != ScaleType.MATRIX) { 593 | throw new IllegalStateException( 594 | "The ImageView's ScaleType has been changed since attaching a PhotoViewAttacher"); 595 | } 596 | } 597 | } 598 | 599 | private void checkMatrixBounds() { 600 | final ImageView imageView = getImageView(); 601 | if (null == imageView) { 602 | return; 603 | } 604 | 605 | final RectF rect = getDisplayRect(getDisplayMatrix()); 606 | if (null == rect) { 607 | return; 608 | } 609 | 610 | final float height = rect.height(), width = rect.width(); 611 | float deltaX = 0, deltaY = 0; 612 | 613 | final int viewHeight = imageView.getHeight(); 614 | if (height <= viewHeight) { 615 | switch (mScaleType) { 616 | case FIT_START: 617 | deltaY = -rect.top; 618 | break; 619 | case FIT_END: 620 | deltaY = viewHeight - height - rect.top; 621 | break; 622 | default: 623 | deltaY = (viewHeight - height) / 2 - rect.top; 624 | break; 625 | } 626 | } else if (rect.top > 0) { 627 | deltaY = -rect.top; 628 | } else if (rect.bottom < viewHeight) { 629 | deltaY = viewHeight - rect.bottom; 630 | } 631 | 632 | final int viewWidth = imageView.getWidth(); 633 | if (width <= viewWidth) { 634 | switch (mScaleType) { 635 | case FIT_START: 636 | deltaX = -rect.left; 637 | break; 638 | case FIT_END: 639 | deltaX = viewWidth - width - rect.left; 640 | break; 641 | default: 642 | deltaX = (viewWidth - width) / 2 - rect.left; 643 | break; 644 | } 645 | mScrollEdge = EDGE_BOTH; 646 | } else if (rect.left > 0) { 647 | mScrollEdge = EDGE_LEFT; 648 | deltaX = -rect.left; 649 | } else if (rect.right < viewWidth) { 650 | deltaX = viewWidth - rect.right; 651 | mScrollEdge = EDGE_RIGHT; 652 | } else { 653 | mScrollEdge = EDGE_NONE; 654 | } 655 | 656 | // Finally actually translate the matrix 657 | mSuppMatrix.postTranslate(deltaX, deltaY); 658 | } 659 | 660 | /** 661 | * Helper method that maps the supplied Matrix to the current Drawable 662 | * 663 | * @param matrix 664 | * - Matrix to map Drawable against 665 | * @return RectF - Displayed Rectangle 666 | */ 667 | private RectF getDisplayRect(Matrix matrix) { 668 | ImageView imageView = getImageView(); 669 | 670 | if (null != imageView) { 671 | Drawable d = imageView.getDrawable(); 672 | if (null != d) { 673 | mDisplayRect.set(0, 0, d.getIntrinsicWidth(), 674 | d.getIntrinsicHeight()); 675 | matrix.mapRect(mDisplayRect); 676 | return mDisplayRect; 677 | } 678 | } 679 | return null; 680 | } 681 | 682 | /** 683 | * Helper method that 'unpacks' a Matrix and returns the required value 684 | * 685 | * @param matrix 686 | * - Matrix to unpack 687 | * @param whichValue 688 | * - Which value from Matrix.M* to return 689 | * @return float - returned value 690 | */ 691 | private float getValue(Matrix matrix, int whichValue) { 692 | matrix.getValues(mMatrixValues); 693 | return mMatrixValues[whichValue]; 694 | } 695 | 696 | /** 697 | * Resets the Matrix back to FIT_CENTER, and then displays it.s 698 | */ 699 | private void resetMatrix() { 700 | mSuppMatrix.reset(); 701 | setImageViewMatrix(getDisplayMatrix()); 702 | checkMatrixBounds(); 703 | } 704 | 705 | private void setImageViewMatrix(Matrix matrix) { 706 | ImageView imageView = getImageView(); 707 | if (null != imageView) { 708 | 709 | checkImageViewScaleType(); 710 | imageView.setImageMatrix(matrix); 711 | 712 | // Call MatrixChangedListener if needed 713 | if (null != mMatrixChangeListener) { 714 | RectF displayRect = getDisplayRect(matrix); 715 | if (null != displayRect) { 716 | mMatrixChangeListener.onMatrixChanged(displayRect); 717 | } 718 | } 719 | } 720 | } 721 | 722 | /** 723 | * Calculate Matrix for FIT_CENTER 724 | * 725 | * @param d 726 | * - Drawable being displayed 727 | */ 728 | private void updateBaseMatrix(Drawable d) { 729 | ImageView imageView = getImageView(); 730 | if (null == imageView || null == d) { 731 | return; 732 | } 733 | 734 | final float viewWidth = imageView.getWidth(); 735 | final float viewHeight = imageView.getHeight(); 736 | final int drawableWidth = d.getIntrinsicWidth(); 737 | final int drawableHeight = d.getIntrinsicHeight(); 738 | 739 | mBaseMatrix.reset(); 740 | 741 | final float widthScale = viewWidth / drawableWidth; 742 | final float heightScale = viewHeight / drawableHeight; 743 | 744 | if (mScaleType == ScaleType.CENTER) { 745 | mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F, 746 | (viewHeight - drawableHeight) / 2F); 747 | 748 | } else if (mScaleType == ScaleType.CENTER_CROP) { 749 | float scale = Math.max(widthScale, heightScale); 750 | mBaseMatrix.postScale(scale, scale); 751 | mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, 752 | (viewHeight - drawableHeight * scale) / 2F); 753 | 754 | } else if (mScaleType == ScaleType.CENTER_INSIDE) { 755 | float scale = Math.min(1.0f, Math.min(widthScale, heightScale)); 756 | mBaseMatrix.postScale(scale, scale); 757 | mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, 758 | (viewHeight - drawableHeight * scale) / 2F); 759 | 760 | } else { 761 | RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight); 762 | RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight); 763 | 764 | switch (mScaleType) { 765 | case FIT_CENTER: 766 | mBaseMatrix 767 | .setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER); 768 | break; 769 | 770 | case FIT_START: 771 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START); 772 | break; 773 | 774 | case FIT_END: 775 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END); 776 | break; 777 | 778 | case FIT_XY: 779 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL); 780 | break; 781 | 782 | default: 783 | break; 784 | } 785 | } 786 | 787 | resetMatrix(); 788 | } 789 | 790 | /** 791 | * Interface definition for a callback to be invoked when the internal 792 | * Matrix has changed for this View. 793 | * 794 | * @author Chris Banes 795 | */ 796 | public static interface OnMatrixChangedListener { 797 | /** 798 | * Callback for when the Matrix displaying the Drawable has changed. 799 | * This could be because the View's bounds have changed, or the user has 800 | * zoomed. 801 | * 802 | * @param rect 803 | * - Rectangle displaying the Drawable's new bounds. 804 | */ 805 | void onMatrixChanged(RectF rect); 806 | } 807 | 808 | /** 809 | * Interface definition for a callback to be invoked when the Photo is 810 | * tapped with a single tap. 811 | * 812 | * @author Chris Banes 813 | */ 814 | public static interface OnPhotoTapListener { 815 | 816 | /** 817 | * A callback to receive where the user taps on a photo. You will only 818 | * receive a callback if the user taps on the actual photo, tapping on 819 | * 'whitespace' will be ignored. 820 | * 821 | * @param view 822 | * - View the user tapped. 823 | * @param x 824 | * - where the user tapped from the of the Drawable, as 825 | * percentage of the Drawable width. 826 | * @param y 827 | * - where the user tapped from the top of the Drawable, as 828 | * percentage of the Drawable height. 829 | */ 830 | void onPhotoTap(View view, float x, float y); 831 | } 832 | 833 | /** 834 | * Interface definition for a callback to be invoked when the ImageView is 835 | * tapped with a single tap. 836 | * 837 | * @author Chris Banes 838 | */ 839 | public static interface OnViewTapListener { 840 | 841 | /** 842 | * A callback to receive where the user taps on a ImageView. You will 843 | * receive a callback if the user taps anywhere on the view, tapping on 844 | * 'whitespace' will not be ignored. 845 | * 846 | * @param view 847 | * - View the user tapped. 848 | * @param x 849 | * - where the user tapped from the left of the View. 850 | * @param y 851 | * - where the user tapped from the top of the View. 852 | */ 853 | void onViewTap(View view, float x, float y); 854 | } 855 | 856 | private class AnimatedZoomRunnable implements Runnable { 857 | 858 | // These are 'postScale' values, means they're compounded each iteration 859 | static final float ANIMATION_SCALE_PER_ITERATION_IN = 1.07f; 860 | static final float ANIMATION_SCALE_PER_ITERATION_OUT = 0.93f; 861 | 862 | private final float mFocalX, mFocalY; 863 | private final float mTargetZoom; 864 | private final float mDeltaScale; 865 | 866 | public AnimatedZoomRunnable(final float currentZoom, 867 | final float targetZoom, final float focalX, final float focalY) { 868 | mTargetZoom = targetZoom; 869 | mFocalX = focalX; 870 | mFocalY = focalY; 871 | 872 | if (currentZoom < targetZoom) { 873 | mDeltaScale = ANIMATION_SCALE_PER_ITERATION_IN; 874 | } else { 875 | mDeltaScale = ANIMATION_SCALE_PER_ITERATION_OUT; 876 | } 877 | } 878 | 879 | public void run() { 880 | ImageView imageView = getImageView(); 881 | 882 | if (null != imageView) { 883 | mSuppMatrix.postScale(mDeltaScale, mDeltaScale, mFocalX, 884 | mFocalY); 885 | checkAndDisplayMatrix(); 886 | 887 | final float currentScale = getScale(); 888 | 889 | if ((mDeltaScale > 1f && currentScale < mTargetZoom) 890 | || (mDeltaScale < 1f && mTargetZoom < currentScale)) { 891 | // We haven't hit our target scale yet, so post ourselves 892 | // again 893 | Compat.postOnAnimation(imageView, this); 894 | 895 | } else { 896 | // We've scaled past our target zoom, so calculate the 897 | // necessary scale so we're back at target zoom 898 | final float delta = mTargetZoom / currentScale; 899 | mSuppMatrix.postScale(delta, delta, mFocalX, mFocalY); 900 | checkAndDisplayMatrix(); 901 | } 902 | } 903 | } 904 | } 905 | 906 | private class FlingRunnable implements Runnable { 907 | 908 | private final ScrollerProxy mScroller; 909 | private int mCurrentX, mCurrentY; 910 | 911 | public FlingRunnable(Context context) { 912 | mScroller = ScrollerProxy.getScroller(context); 913 | } 914 | 915 | public void cancelFling() { 916 | if (DEBUG) { 917 | Log.d(LOG_TAG, "Cancel Fling"); 918 | } 919 | mScroller.forceFinished(true); 920 | } 921 | 922 | public void fling(int viewWidth, int viewHeight, int velocityX, 923 | int velocityY) { 924 | final RectF rect = getDisplayRect(); 925 | if (null == rect) { 926 | return; 927 | } 928 | 929 | final int startX = Math.round(-rect.left); 930 | final int minX, maxX, minY, maxY; 931 | 932 | if (viewWidth < rect.width()) { 933 | minX = 0; 934 | maxX = Math.round(rect.width() - viewWidth); 935 | } else { 936 | minX = maxX = startX; 937 | } 938 | 939 | final int startY = Math.round(-rect.top); 940 | if (viewHeight < rect.height()) { 941 | minY = 0; 942 | maxY = Math.round(rect.height() - viewHeight); 943 | } else { 944 | minY = maxY = startY; 945 | } 946 | 947 | mCurrentX = startX; 948 | mCurrentY = startY; 949 | 950 | if (DEBUG) { 951 | Log.d(LOG_TAG, "fling. StartX:" + startX + " StartY:" + startY 952 | + " MaxX:" + maxX + " MaxY:" + maxY); 953 | } 954 | 955 | // If we actually can move, fling the scroller 956 | if (startX != maxX || startY != maxY) { 957 | mScroller.fling(startX, startY, velocityX, velocityY, minX, 958 | maxX, minY, maxY, 0, 0); 959 | } 960 | } 961 | 962 | @Override 963 | public void run() { 964 | ImageView imageView = getImageView(); 965 | if (null != imageView && mScroller.computeScrollOffset()) { 966 | 967 | final int newX = mScroller.getCurrX(); 968 | final int newY = mScroller.getCurrY(); 969 | 970 | if (DEBUG) { 971 | Log.d(LOG_TAG, "fling run(). CurrentX:" + mCurrentX 972 | + " CurrentY:" + mCurrentY + " NewX:" + newX 973 | + " NewY:" + newY); 974 | } 975 | 976 | mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY); 977 | setImageViewMatrix(getDisplayMatrix()); 978 | 979 | mCurrentX = newX; 980 | mCurrentY = newY; 981 | 982 | // Post On animation 983 | Compat.postOnAnimation(imageView, this); 984 | } 985 | } 986 | } 987 | } 988 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/zoom/SDK16.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.zoom; 2 | 3 | import android.annotation.TargetApi; 4 | import android.view.View; 5 | 6 | @TargetApi(16) 7 | public class SDK16 { 8 | 9 | public static void postOnAnimation(View view, Runnable r) { 10 | view.postOnAnimation(r); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/zoom/ScrollerProxy.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.zoom; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.os.Build.VERSION; 6 | import android.os.Build.VERSION_CODES; 7 | import android.widget.OverScroller; 8 | import android.widget.Scroller; 9 | 10 | public abstract class ScrollerProxy { 11 | 12 | public static ScrollerProxy getScroller(Context context) { 13 | if (VERSION.SDK_INT < VERSION_CODES.GINGERBREAD) { 14 | return new PreGingerScroller(context); 15 | } else { 16 | return new GingerScroller(context); 17 | } 18 | } 19 | 20 | public abstract boolean computeScrollOffset(); 21 | 22 | public abstract void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, 23 | int maxY, int overX, int overY); 24 | 25 | public abstract void forceFinished(boolean finished); 26 | 27 | public abstract int getCurrX(); 28 | 29 | public abstract int getCurrY(); 30 | 31 | @TargetApi(9) 32 | private static class GingerScroller extends ScrollerProxy { 33 | 34 | private OverScroller mScroller; 35 | 36 | public GingerScroller(Context context) { 37 | mScroller = new OverScroller(context); 38 | } 39 | 40 | @Override 41 | public boolean computeScrollOffset() { 42 | return mScroller.computeScrollOffset(); 43 | } 44 | 45 | @Override 46 | public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, 47 | int overX, int overY) { 48 | mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, overX, overY); 49 | } 50 | 51 | @Override 52 | public void forceFinished(boolean finished) { 53 | mScroller.forceFinished(finished); 54 | } 55 | 56 | @Override 57 | public int getCurrX() { 58 | return mScroller.getCurrX(); 59 | } 60 | 61 | @Override 62 | public int getCurrY() { 63 | return mScroller.getCurrY(); 64 | } 65 | } 66 | 67 | private static class PreGingerScroller extends ScrollerProxy { 68 | 69 | private Scroller mScroller; 70 | 71 | public PreGingerScroller(Context context) { 72 | mScroller = new Scroller(context); 73 | } 74 | 75 | @Override 76 | public boolean computeScrollOffset() { 77 | return mScroller.computeScrollOffset(); 78 | } 79 | 80 | @Override 81 | public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, 82 | int overX, int overY) { 83 | mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY); 84 | } 85 | 86 | @Override 87 | public void forceFinished(boolean finished) { 88 | mScroller.forceFinished(finished); 89 | } 90 | 91 | @Override 92 | public int getCurrX() { 93 | return mScroller.getCurrX(); 94 | } 95 | 96 | @Override 97 | public int getCurrY() { 98 | return mScroller.getCurrY(); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/zoom/VersionedGestureDetector.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.zoom; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.util.FloatMath; 7 | import android.view.MotionEvent; 8 | import android.view.ScaleGestureDetector; 9 | import android.view.ScaleGestureDetector.OnScaleGestureListener; 10 | import android.view.VelocityTracker; 11 | import android.view.ViewConfiguration; 12 | 13 | public abstract class VersionedGestureDetector { 14 | static final String LOG_TAG = "VersionedGestureDetector"; 15 | OnGestureListener mListener; 16 | 17 | public static VersionedGestureDetector newInstance(Context context, OnGestureListener listener) { 18 | final int sdkVersion = Build.VERSION.SDK_INT; 19 | VersionedGestureDetector detector = null; 20 | 21 | if (sdkVersion < Build.VERSION_CODES.ECLAIR) { 22 | detector = new CupcakeDetector(context); 23 | } else if (sdkVersion < Build.VERSION_CODES.FROYO) { 24 | detector = new EclairDetector(context); 25 | } else { 26 | detector = new FroyoDetector(context); 27 | } 28 | 29 | detector.mListener = listener; 30 | 31 | return detector; 32 | } 33 | 34 | public abstract boolean onTouchEvent(MotionEvent ev); 35 | 36 | public abstract boolean isScaling(); 37 | 38 | public static interface OnGestureListener { 39 | public void onDrag(float dx, float dy); 40 | 41 | public void onFling(float startX, float startY, float velocityX, float velocityY); 42 | 43 | public void onScale(float scaleFactor, float focusX, float focusY); 44 | } 45 | 46 | private static class CupcakeDetector extends VersionedGestureDetector { 47 | 48 | float mLastTouchX; 49 | float mLastTouchY; 50 | final float mTouchSlop; 51 | final float mMinimumVelocity; 52 | 53 | public CupcakeDetector(Context context) { 54 | final ViewConfiguration configuration = ViewConfiguration.get(context); 55 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 56 | mTouchSlop = configuration.getScaledTouchSlop(); 57 | } 58 | 59 | private VelocityTracker mVelocityTracker; 60 | private boolean mIsDragging; 61 | 62 | float getActiveX(MotionEvent ev) { 63 | return ev.getX(); 64 | } 65 | 66 | float getActiveY(MotionEvent ev) { 67 | return ev.getY(); 68 | } 69 | 70 | public boolean isScaling() { 71 | return false; 72 | } 73 | 74 | @Override 75 | public boolean onTouchEvent(MotionEvent ev) { 76 | switch (ev.getAction()) { 77 | case MotionEvent.ACTION_DOWN: { 78 | mVelocityTracker = VelocityTracker.obtain(); 79 | mVelocityTracker.addMovement(ev); 80 | 81 | mLastTouchX = getActiveX(ev); 82 | mLastTouchY = getActiveY(ev); 83 | mIsDragging = false; 84 | break; 85 | } 86 | 87 | case MotionEvent.ACTION_MOVE: { 88 | final float x = getActiveX(ev); 89 | final float y = getActiveY(ev); 90 | final float dx = x - mLastTouchX, dy = y - mLastTouchY; 91 | 92 | if (!mIsDragging) { 93 | // Use Pythagoras to see if drag length is larger than 94 | // touch slop 95 | mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop; 96 | } 97 | 98 | if (mIsDragging) { 99 | mListener.onDrag(dx, dy); 100 | mLastTouchX = x; 101 | mLastTouchY = y; 102 | 103 | if (null != mVelocityTracker) { 104 | mVelocityTracker.addMovement(ev); 105 | } 106 | } 107 | break; 108 | } 109 | 110 | case MotionEvent.ACTION_CANCEL: { 111 | // Recycle Velocity Tracker 112 | if (null != mVelocityTracker) { 113 | mVelocityTracker.recycle(); 114 | mVelocityTracker = null; 115 | } 116 | break; 117 | } 118 | 119 | case MotionEvent.ACTION_UP: { 120 | if (mIsDragging) { 121 | if (null != mVelocityTracker) { 122 | mLastTouchX = getActiveX(ev); 123 | mLastTouchY = getActiveY(ev); 124 | 125 | // Compute velocity within the last 1000ms 126 | mVelocityTracker.addMovement(ev); 127 | mVelocityTracker.computeCurrentVelocity(1000); 128 | 129 | final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker.getYVelocity(); 130 | 131 | // If the velocity is greater than minVelocity, call 132 | // listener 133 | if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) { 134 | mListener.onFling(mLastTouchX, mLastTouchY, -vX, -vY); 135 | } 136 | } 137 | } 138 | 139 | // Recycle Velocity Tracker 140 | if (null != mVelocityTracker) { 141 | mVelocityTracker.recycle(); 142 | mVelocityTracker = null; 143 | } 144 | break; 145 | } 146 | } 147 | 148 | return true; 149 | } 150 | } 151 | 152 | @TargetApi(5) 153 | private static class EclairDetector extends CupcakeDetector { 154 | private static final int INVALID_POINTER_ID = -1; 155 | private int mActivePointerId = INVALID_POINTER_ID; 156 | private int mActivePointerIndex = 0; 157 | 158 | public EclairDetector(Context context) { 159 | super(context); 160 | } 161 | 162 | @Override 163 | float getActiveX(MotionEvent ev) { 164 | try { 165 | return ev.getX(mActivePointerIndex); 166 | } catch (Exception e) { 167 | return ev.getX(); 168 | } 169 | } 170 | 171 | @Override 172 | float getActiveY(MotionEvent ev) { 173 | try { 174 | return ev.getY(mActivePointerIndex); 175 | } catch (Exception e) { 176 | return ev.getY(); 177 | } 178 | } 179 | 180 | @Override 181 | public boolean onTouchEvent(MotionEvent ev) { 182 | final int action = ev.getAction(); 183 | switch (action & MotionEvent.ACTION_MASK) { 184 | case MotionEvent.ACTION_DOWN: 185 | mActivePointerId = ev.getPointerId(0); 186 | break; 187 | case MotionEvent.ACTION_CANCEL: 188 | case MotionEvent.ACTION_UP: 189 | mActivePointerId = INVALID_POINTER_ID; 190 | break; 191 | case MotionEvent.ACTION_POINTER_UP: 192 | final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; 193 | final int pointerId = ev.getPointerId(pointerIndex); 194 | if (pointerId == mActivePointerId) { 195 | // This was our active pointer going up. Choose a new 196 | // active pointer and adjust accordingly. 197 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 198 | mActivePointerId = ev.getPointerId(newPointerIndex); 199 | mLastTouchX = ev.getX(newPointerIndex); 200 | mLastTouchY = ev.getY(newPointerIndex); 201 | } 202 | break; 203 | } 204 | 205 | mActivePointerIndex = ev.findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId : 0); 206 | return super.onTouchEvent(ev); 207 | } 208 | } 209 | 210 | @TargetApi(8) 211 | private static class FroyoDetector extends EclairDetector { 212 | 213 | private final ScaleGestureDetector mDetector; 214 | 215 | // Needs to be an inner class so that we don't hit 216 | // VerifyError's on API 4. 217 | private final OnScaleGestureListener mScaleListener = new OnScaleGestureListener() { 218 | 219 | @Override 220 | public boolean onScale(ScaleGestureDetector detector) { 221 | mListener.onScale(detector.getScaleFactor(), detector.getFocusX(), detector.getFocusY()); 222 | return true; 223 | } 224 | 225 | @Override 226 | public boolean onScaleBegin(ScaleGestureDetector detector) { 227 | return true; 228 | } 229 | 230 | @Override 231 | public void onScaleEnd(ScaleGestureDetector detector) { 232 | // NO-OP 233 | } 234 | }; 235 | 236 | public FroyoDetector(Context context) { 237 | super(context); 238 | mDetector = new ScaleGestureDetector(context, mScaleListener); 239 | } 240 | 241 | @Override 242 | public boolean isScaling() { 243 | return mDetector.isInProgress(); 244 | } 245 | 246 | @Override 247 | public boolean onTouchEvent(MotionEvent ev) { 248 | mDetector.onTouchEvent(ev); 249 | return super.onTouchEvent(ev); 250 | } 251 | 252 | } 253 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/mjj/selectphotodemo/zoom/ViewPagerFixed.java: -------------------------------------------------------------------------------- 1 | package com.example.mjj.selectphotodemo.zoom; 2 | 3 | import android.content.Context; 4 | import android.support.v4.view.ViewPager; 5 | import android.util.AttributeSet; 6 | import android.view.MotionEvent; 7 | 8 | /** 9 | * Description:自定义ViewPager对触摸事件进行处理 10 | *

11 | * Created by Mjj on 2016/12/2. 12 | */ 13 | 14 | public class ViewPagerFixed extends ViewPager { 15 | 16 | public ViewPagerFixed(Context context) { 17 | super(context); 18 | } 19 | 20 | public ViewPagerFixed(Context context, AttributeSet attrs) { 21 | super(context, attrs); 22 | } 23 | 24 | @Override 25 | public boolean onTouchEvent(MotionEvent ev) { 26 | try { 27 | return super.onTouchEvent(ev); 28 | } catch (IllegalArgumentException ex) { 29 | ex.printStackTrace(); 30 | } 31 | return false; 32 | } 33 | 34 | @Override 35 | public boolean onInterceptTouchEvent(MotionEvent ev) { 36 | try { 37 | return super.onInterceptTouchEvent(ev); 38 | } catch (IllegalArgumentException ex) { 39 | ex.printStackTrace(); 40 | } 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/res/anim/anim_entry_from_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/anim/anim_leave_from_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/color/default_text_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/color/floder_name_color_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/btn_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Mu/SelectPhotoDemo/be9314374e3ed451863ad7108bcec1ab4d7a1626/app/src/main/res/drawable-xhdpi/btn_back.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/btn_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Mu/SelectPhotoDemo/be9314374e3ed451863ad7108bcec1ab4d7a1626/app/src/main/res/drawable-xhdpi/btn_selected.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/btn_unselected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Mu/SelectPhotoDemo/be9314374e3ed451863ad7108bcec1ab4d7a1626/app/src/main/res/drawable-xhdpi/btn_unselected.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Mu/SelectPhotoDemo/be9314374e3ed451863ad7108bcec1ab4d7a1626/app/src/main/res/drawable-xhdpi/ic_camera.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_delete_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Mu/SelectPhotoDemo/be9314374e3ed451863ad7108bcec1ab4d7a1626/app/src/main/res/drawable-xhdpi/ic_delete_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_dir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Mu/SelectPhotoDemo/be9314374e3ed451863ad7108bcec1ab4d7a1626/app/src/main/res/drawable-xhdpi/ic_dir.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_dir_choose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Mu/SelectPhotoDemo/be9314374e3ed451863ad7108bcec1ab4d7a1626/app/src/main/res/drawable-xhdpi/ic_dir_choose.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Mu/SelectPhotoDemo/be9314374e3ed451863ad7108bcec1ab4d7a1626/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_photo_loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Mu/SelectPhotoDemo/be9314374e3ed451863ad7108bcec1ab4d7a1626/app/src/main/res/drawable-xhdpi/ic_photo_loading.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/icon_addpic_focused.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Mu/SelectPhotoDemo/be9314374e3ed451863ad7108bcec1ab4d7a1626/app/src/main/res/drawable-xhdpi/icon_addpic_focused.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/text_indicator_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Mu/SelectPhotoDemo/be9314374e3ed451863ad7108bcec1ab4d7a1626/app/src/main/res/drawable-xhdpi/text_indicator_normal.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/text_indicator_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Mu/SelectPhotoDemo/be9314374e3ed451863ad7108bcec1ab4d7a1626/app/src/main/res/drawable-xhdpi/text_indicator_pressed.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/action_btn.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_camera_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/btn_select_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/layout_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/scrollbar_vertical_thumb.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/text_indicator_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_follow_wechat_photo.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_photo_picker.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 15 | 16 | 26 | 27 | 31 | 32 | 38 | 39 | 50 | 51 | 61 | 62 | 63 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fload_list_layout_stub.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/folderlist_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_camera_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_folder_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 24 | 25 | 32 | 33 | 41 | 42 | 43 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_gridview.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_photo_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 19 | 20 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/plugin_camera_gallery.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | 13 | 22 | 23 | 30 | 31 | 32 | 38 | 39 |