PhotoView的使用方法
PhotoView 是一款扩展自Android ImageView ,支持通过单点/多点触摸来进行图片缩放的智能控件。
项目地址:https://github.com/chrisbanes/PhotoView
特性
- 支持单点/多点触摸、双击,即时缩放图片;
- 支持平滑滚动;
- 在滑动父控件下能够运行良好;(例如:ViewPager)
- 当用户的触点改变是可以触发通知
Gradle中依赖
第1步:
在根build.gradle
(不是module的 build.gradle
)文件中添加以下内容:
allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}
第2步:
在module 的build.gradle
文件中添加以下内容:
dependencies {
compile 'com.github.chrisbanes:PhotoView:1.3.0'
}
示例代码
示例代码来自 https://github.com/chrisbanes/PhotoView/tree/master/sample
有一点点修改的内容
基本使用
activity_simple_sample.xml布局文件:
<?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的代码:
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();
}
}
从代码中,可以看到以下几点基本内容:
- 可以直接在布局文件中使用PhotoView
- 在Activity中,可以用ImageView来获取布局文件中的PhotoView
- 通过PhotoViewAttacher来对PhotoView进行控制:
- 通过
setScaleType
方法设置ScaleType - 通过
setScale
方法进行带有动画的缩放 - 通过
getDisplayMatrix
和setDisplayMatrix
获得与设置显示的Matrix - 通过
getVisibleRectangleBitmap
方法获得可视部分的Bitmap对象
- 通过
- 通过PhotoViewAttacher来对PhotoView进行事件响应:
- GestureDetector.OnDoubleTapListener 双击事件
- OnScaleChangeListener scale改变事件
- OnSingleFlingListener 单个触摸的滑动事件
- OnLongClickListener 长按事件
- OnMatrixChangedListener Matrix改变监听事件
- OnPhotoTapListener 图片点击事件
在ViewGroup中使用
在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);
}
}
从代码中可以看出:
- setRotationBy来设置相对旋转
- setRotationTo来设置绝对旋转
与Picasso结合使用
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间跳转时的转换效果
起始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必须是一样的 -->