PhotoView 是一款擴展自Android ImageView ,支持經過單點/多點觸摸來進行圖片縮放的智能控件。java
項目地址:https://github.com/chrisbanes/PhotoViewandroid
第1步:git
在根build.gradle
(不是module的 build.gradle
)文件中添加如下內容:github
allprojects { repositories { ... maven { url "https://jitpack.io" } } }
第2步:api
在module 的build.gradle
文件中添加如下內容:app
dependencies { compile 'com.github.chrisbanes:PhotoView:1.3.0' }
示例代碼來自 https://github.com/chrisbanes/PhotoView/tree/master/sample框架
有一點點修改的內容dom
activity_simple_sample.xml佈局文件:maven
<?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android"> <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent"> <uk.co.senab.photoview.PhotoView android:id="@+id/iv_photo" android:layout_width="fill_parent" android:layout_height="fill_parent" /> <TextView android:id="@+id/tv_current_matrix" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_gravity="bottom|center_horizontal" android:background="#60000000" android:gravity="center" android:textColor="@android:color/white" /> </FrameLayout> </merge>
SimpleSampleActivity.java的代碼:ide
public class SimpleSampleActivity extends AppCompatActivity { static final String PHOTO_TAP_TOAST_STRING = "Photo Tap! X: %.2f %% Y:%.2f %% ID: %d"; static final String SCALE_TOAST_STRING = "Scaled to: %.2ff"; static final String FLING_LOG_STRING = "Fling velocityX: %.2f, velocityY: %.2f"; private TextView mCurrMatrixTv; private PhotoViewAttacher mAttacher; private Toast mCurrentToast; private Matrix mCurrentDisplayMatrix = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_simple_sample); //PhotoVIew是ImageView的子類 ImageView mImageView = (ImageView) findViewById(R.id.iv_photo); mCurrMatrixTv = (TextView) findViewById(R.id.tv_current_matrix); Drawable bitmap = ContextCompat.getDrawable(this, R.drawable.wallpaper); mImageView.setImageDrawable(bitmap); // The MAGIC happens here! //PhotoViewAttacher是整個庫裏比較重要的一個類,它控制它所附着的PhotoView mAttacher = new PhotoViewAttacher(mImageView); // Lets attach some listeners, not required though! //給mAttacher添加各類事件 mAttacher.setOnMatrixChangeListener(new MatrixChangeListener()); mAttacher.setOnPhotoTapListener(new PhotoTapListener()); mAttacher.setOnSingleFlingListener(new SingleFlingListener()); } @Override public void onDestroy() { super.onDestroy(); // Need to call clean-up //它所附着的PhotoView再也不使用的時候,進行清理 mAttacher.cleanup(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main_menu, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onPrepareOptionsMenu(Menu menu) { MenuItem zoomToggle = menu.findItem(R.id.menu_zoom_toggle); assert null != zoomToggle; zoomToggle.setTitle(mAttacher.canZoom() ? R.string.menu_zoom_disable : R.string.menu_zoom_enable); return super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_zoom_toggle: //設置是否能夠縮放 mAttacher.setZoomable(!mAttacher.canZoom()); return true; case R.id.menu_scale_fit_center: //設置ScaleType mAttacher.setScaleType(ImageView.ScaleType.FIT_CENTER); return true; case R.id.menu_scale_fit_start: mAttacher.setScaleType(ImageView.ScaleType.FIT_START); return true; case R.id.menu_scale_fit_end: mAttacher.setScaleType(ImageView.ScaleType.FIT_END); return true; case R.id.menu_scale_fit_xy: mAttacher.setScaleType(ImageView.ScaleType.FIT_XY); return true; case R.id.menu_scale_scale_center: mAttacher.setScaleType(ImageView.ScaleType.CENTER); return true; case R.id.menu_scale_scale_center_crop: mAttacher.setScaleType(ImageView.ScaleType.CENTER_CROP); return true; case R.id.menu_scale_scale_center_inside: mAttacher.setScaleType(ImageView.ScaleType.CENTER_INSIDE); return true; case R.id.menu_scale_random_animate: case R.id.menu_scale_random: Random r = new Random(); float minScale = mAttacher.getMinimumScale(); float maxScale = mAttacher.getMaximumScale(); float randomScale = minScale + (r.nextFloat() * (maxScale - minScale)); //設置縮放與動畫 mAttacher.setScale(randomScale, item.getItemId() == R.id.menu_scale_random_animate); showToast(String.format(SCALE_TOAST_STRING, randomScale)); return true; case R.id.menu_matrix_restore: if (mCurrentDisplayMatrix == null) showToast("You need to capture display matrix first"); else mAttacher.setDisplayMatrix(mCurrentDisplayMatrix); return true; case R.id.menu_matrix_capture: //這段代碼有問題 if (mCurrentDisplayMatrix == null) mCurrentDisplayMatrix = new Matrix(); mAttacher.getDisplayMatrix(mCurrentDisplayMatrix); return true; case R.id.extract_visible_bitmap: try { //經過mAttacher得到可視部分的Bitmap對象 Bitmap bmp = mAttacher.getVisibleRectangleBitmap(); File tmpFile = File.createTempFile("photoview", ".png", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)); FileOutputStream out = new FileOutputStream(tmpFile); bmp.compress(Bitmap.CompressFormat.PNG, 90, out); out.close(); Intent share = new Intent(Intent.ACTION_SEND); share.setType("image/png"); share.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(tmpFile)); startActivity(share); Toast.makeText(this, String.format("Extracted into: %s", tmpFile.getAbsolutePath()), Toast.LENGTH_SHORT).show(); } catch (Throwable t) { t.printStackTrace(); Toast.makeText(this, "Error occured while extracting bitmap", Toast.LENGTH_SHORT).show(); } return true; } return super.onOptionsItemSelected(item); } /** * 點擊一下的事件 */ private class PhotoTapListener implements PhotoViewAttacher.OnPhotoTapListener { @Override public void onPhotoTap(View view, float x, float y) { float xPercentage = x * 100f; float yPercentage = y * 100f; showToast(String.format(PHOTO_TAP_TOAST_STRING, xPercentage, yPercentage, view == null ? 0 : view.getId())); } @Override public void onOutsidePhotoTap() { showToast("You have a tap event on the place where out of the photo."); } } /** * Matrix改變監聽事件 */ private class MatrixChangeListener implements PhotoViewAttacher.OnMatrixChangedListener { @Override public void onMatrixChanged(RectF rect) { mCurrMatrixTv.setText(rect.toString()); } } /** * 單個觸摸的滑動事件 */ private class SingleFlingListener implements PhotoViewAttacher.OnSingleFlingListener { @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (BuildConfig.DEBUG) { Log.d("PhotoView", String.format(FLING_LOG_STRING, velocityX, velocityY)); } return true; } } private void showToast(CharSequence text) { if (null != mCurrentToast) { mCurrentToast.cancel(); } mCurrentToast = Toast.makeText(SimpleSampleActivity.this, text, Toast.LENGTH_SHORT); mCurrentToast.show(); } }
從代碼中,能夠看到如下幾點基本內容:
setScaleType
方法設置ScaleTypesetScale
方法進行帶有動畫的縮放getDisplayMatrix
和setDisplayMatrix
得到與設置顯示的MatrixgetVisibleRectangleBitmap
方法得到可視部分的Bitmap對象在ViewGroup(使用了onInterceptTouchEvent
)中使用PhotoView會拋異常,最多見的是ViewPager與DrawerLayout。這個是框架自己的問題,尚未解決。爲了防止這個異常,就是簡單的catch住這個異常就能夠了。這是一個基本的代碼模板,ProblematicViewGroup
表明會出現問題的ViewGroup(如ViewPager和DrawerLayout):
public class HackyProblematicViewGroup extends ProblematicViewGroup { public HackyProblematicViewGroup(Context context) { super(context); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { try { return super.onInterceptTouchEvent(ev); } catch (IllegalArgumentException e) { //uncomment if you really want to see these errors //e.printStackTrace(); return false; } } }
例如,若是使用的是ViewPager,那麼這樣:
/** * Hacky fix for Issue #4 and * http://code.google.com/p/android/issues/detail?id=18990 * <p/> * ScaleGestureDetector seems to mess up the touch events, which means that * ViewGroups which make use of onInterceptTouchEvent throw a lot of * IllegalArgumentException: pointerIndex out of range. * <p/> * There's not much I can do in my code for now, but we can mask the result by * just catching the problem and ignoring it. * * @author Chris Banes */ public class HackyViewPager extends ViewPager { public HackyViewPager(Context context) { super(context); } public HackyViewPager(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { try { return super.onInterceptTouchEvent(ev); } catch (IllegalArgumentException e) { e.printStackTrace(); return false; } } }
若是使用DrawerLayout,那麼:
/** * Hacky fix for Issue #4 and * http://code.google.com/p/android/issues/detail?id=18990 * <p/> * ScaleGestureDetector seems to mess up the touch events, which means that * ViewGroups which make use of onInterceptTouchEvent throw a lot of * IllegalArgumentException: pointerIndex out of range. * <p/> * There's not much I can do in my code for now, but we can mask the result by * just catching the problem and ignoring it. * Created by John on 10/1/15. */ public class HackyDrawerLayout extends DrawerLayout { public HackyDrawerLayout(Context context) { super(context); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { try { return super.onInterceptTouchEvent(ev); } catch (IllegalArgumentException e) { e.printStackTrace(); return false; } } }
RotationSampleActivity.java:
public class RotationSampleActivity extends AppCompatActivity { private PhotoView photo; private final Handler handler = new Handler(); private boolean rotating = false; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); photo = new PhotoView(this); photo.setImageResource(R.drawable.wallpaper); //整個佈局就是一個PhotoView setContentView(photo); } @Override protected void onPause() { super.onPause(); handler.removeCallbacksAndMessages(null); } @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add(Menu.NONE, 0, Menu.NONE, "Rotate 10° Right"); menu.add(Menu.NONE, 1, Menu.NONE, "Rotate 10° Left"); menu.add(Menu.NONE, 2, Menu.NONE, "Toggle automatic rotation"); menu.add(Menu.NONE, 3, Menu.NONE, "Reset to 0"); menu.add(Menu.NONE, 4, Menu.NONE, "Reset to 90"); menu.add(Menu.NONE, 5, Menu.NONE, "Reset to 180"); menu.add(Menu.NONE, 6, Menu.NONE, "Reset to 270"); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case 0: //相對旋轉Degree to rotate PhotoView to, should be in range 0 to 360 photo.setRotationBy(10); return true; case 1: photo.setRotationBy(-10); return true; case 2: toggleRotation(); return true; case 3: //絕對旋轉Degree to rotate PhotoView to, should be in range 0 to 360 photo.setRotationTo(0); return true; case 4: photo.setRotationTo(90); return true; case 5: photo.setRotationTo(180); return true; case 6: photo.setRotationTo(270); return true; } return super.onOptionsItemSelected(item); } private void toggleRotation() { if (rotating) { handler.removeCallbacksAndMessages(null);//取消旋轉 } else { rotateLoop(); } rotating = !rotating; } private void rotateLoop() { handler.postDelayed(new Runnable() { @Override public void run() { //每次轉一度 photo.setRotationBy(1); //一直轉 rotateLoop(); } }, 15); } }
從代碼中能夠看出:
PicassoSampleActivity.java的代碼以下,值得注意的是將圖片內容顯示到PhotoView以後,須要更新PhotoViewAttacher 對象
public class PicassoSampleActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_picasso_sample); //獲取佈局文件中的PhotoView PhotoView photoView = (PhotoView) findViewById(R.id.iv_photo); //聲名 PhotoViewAttacher 對象 final PhotoViewAttacher attacher = new PhotoViewAttacher(photoView); //使用Picasso加載圖片 Picasso.with(this) .load("http://ww4.sinaimg.cn/large/610dc034jw1f8xz7ip2u5j20u011h78h.jpg") .into(photoView, new Callback() { @Override public void onSuccess() { //將圖片內容顯示到PhotoView以後,須要更新PhotoViewAttacher 對象 attacher.update(); } @Override public void onError() { } }); } }
起始Activity,它的佈局裏只有一個RecyclerView,RecyclerView的item只有一個ImageView,點擊這個ImageView響應點擊事件,打開另外一個Activity。
起始ActivityTransitionActivity.java的代碼:
public class ActivityTransitionActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_transition); RecyclerView list = (RecyclerView) findViewById(R.id.list); list.setLayoutManager(new GridLayoutManager(this, 2)); ImageAdapter imageAdapter = new ImageAdapter(new ImageAdapter.Listener() { @Override public void onImageClicked(View view) { transition(view); } }); list.setAdapter(imageAdapter); } private void transition(View view) { if (Build.VERSION.SDK_INT < 21) {//只有api level>=21才能夠用這種效果 Toast.makeText(ActivityTransitionActivity.this, "21+ only, keep out", Toast.LENGTH_SHORT).show(); } else {//這種寫法不是最好的,只是爲了說明能夠有這種轉換效果 Intent intent = new Intent(ActivityTransitionActivity.this, ActivityTransitionToActivity.class); ActivityOptionsCompat options = ActivityOptionsCompat. makeSceneTransitionAnimation( ActivityTransitionActivity.this,//起始activity view, //轉換的起始view,這裏就是這個ImageView getString(R.string.transition_test));//view 對應的名字 startActivity(intent, options.toBundle()); } } }
在被啓動的activity中,只有一個PhotoView,它的佈局文件是:
<?xml version="1.0" encoding="utf-8"?> <uk.co.senab.photoview.PhotoView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/iv_photo" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:src="@drawable/wallpaper" android:transitionName="@string/transition_test" /> <!-- 這裏的transittionName與啓動它的Activity裏使用的name必須是同樣的 -->