图片查看是很常见的功能,点击图片之后跳转到另一个界面查看大图,看起来是非常简单,不过自己动手尝试了一下之后并没有想象中的那么顺利,其中还是有很多需要注意的地方。
好了,先看一下效果
对,就是这么简单!显示一张图片,图片可以保存到本地,当然,现在的app基本上都有手势缩放图片的功能,这里我们也要添加这个功能。
好了,功能基本就是这样,下面看代码,首先得界面布局
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#c0000000"
tools:context="me.masteryi.gankio.PhotoActivity">
<ImageView
android:id="@+id/photo_iv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="center"/>
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"/>
android.support.design.widget.AppBarLayout>
FrameLayout>
布局很简单,一个FrameLayout,上面是Appbar,下面是一个ImageView。这里为什么用FrameLayout而不用RelativeLayout呢?主要是为了能够让图片能够全屏显示,可以参考网易新闻的图片界面,这里就不贴图了,你要是喜欢,改成别的布局也不影响。
接下来就是Activity了。这里先提一下用到的第三方库:
其实Picasso和Butterknife是Android Studio自带的,Picasso是Square家的图片加载库,是主流的图片加载库之一,可以搭配同样是Square出品的OkHttp使用。Butterknife配合Butterknife Zelezny插件可以非常方便的完成控件的注入,妈妈再也不用担心我写findViewById了。RxJava/RxAndroid最近非常火,为了跟上时代潮流,我也在努力学习RxJava的用法。PhotoView是一个非常好的图片控制库,可以手势控制图片移动和缩放,并支持双击放大。其实图片缩放本来是想自己实现的,后来发现有这么一个牛逼的库,就偷个懒先用着,以后再补上。
首先是加载图片,这里我们用Picasso实现
Picasso.with(this)
.load(url)
.into(new Target() {
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
photoImageView.setImageBitmap(bitmap);
if (attacher == null) {
attacher = new PhotoViewAttacher(photoImageView);
} else {
attacher.update();
}
}
@Override
public void onBitmapFailed(Drawable errorDrawable) {
Toast.makeText(PhotoActivity.this, "加载图片出错", Toast.LENGTH_SHORT).show();
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
}
});
我这里into方法中用的是一个Target而没有直接用imageView,因为通过Target我们可以得到Bitmap对象,至于为什么要得到这个bitmap对象,我们后面再说。
好了,就这样,一个简单的图片详情页面就做好了。当然,这是最基本的功能,这么可爱的妹纸,当然要保存起来了,so,一般的App都会有保存图片,收藏或者分享功能,这里我们只做下载功能,收藏跟分享以后再补上。
我们新建一个menu
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_more"
android:icon="@drawable/ic_more_vert_white_24dp"
android:title="@string/more"
app:showAsAction="always"
>
<menu>
<item
android:id="@+id/menu_download"
android:icon="@drawable/ic_file_download_black_24dp"
android:title="@string/download"
/>
menu>
item>
menu>
菜单包括溢出菜单,溢出菜单中有一个下载菜单。好,就是这么简单。接下来我们做下载功能。
我们先准备一个保存图片的工具类FileUtil
/**
* Created by Lee
* Date 2016/2/20
* Email [email protected]
* Blog http://masteryi.me
*/
public class FileUtil {
public static final String TAG = "FileUtil";
public static final String IMAGE_PATH = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
.getPath() + File.separator + "Gank";
public static boolean saveImage(String imageName, Bitmap image) {
// Log.d(TAG, "image path:" + IMAGE_PATH);
File file = new File(IMAGE_PATH);
if (!file.exists()) {
file.mkdirs();
}
File imageFile = new File(file, imageName);
try {
if (imageFile.createNewFile()) {
FileOutputStream fos = new FileOutputStream(imageFile);
image.compress(Bitmap.CompressFormat.JPEG, 100, fos);
fos.close();
}
return true;
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
/**
* 通过url获得文件名
*
* @param url 图片url
* @return 图片文件名
*/
public static String url2ImageName(String url) {
String imageName = url.substring(url.lastIndexOf("/") + 1);
return imageName;
}
}
FileUtil很简单,只有两个方法,一个是通过url找到文件名XXX.jpg,一个是保存图片的方法,应该没什么难度。这里有一点要注意的是保存路径我这里用了Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)这个方法,这个其实就是android用来存放公共图片的文件夹,打开这个目录,我们可以看见还有知乎,网易等目录在下面,我们新建一个Gank的目录来存放图片。关于Environment.getExternalStoragePublicDirectory其实不止DIRECTORY_PICTURES一种,还有很多别的类型,具体可以看官方文档(自备梯子)。
接下来我们添加保存图片的方法
private void downloadImage() {
Target target = new Target() {
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
Observable.create((Observable.OnSubscribe) subscriber -> {
// Log.d(TAG, "thread1:" + Thread.currentThread().getName());
String imageName = FileUtil.url2ImageName(url);
subscriber.onNext(FileUtil.saveImage(imageName, bitmap));
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(aBoolean -> {
// Log.d(TAG, "thread2:" + Thread.currentThread().getName());
if (aBoolean) {
Toast.makeText(PhotoActivity.this, "保存图片成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(PhotoActivity.this, "保存图片失败", Toast.LENGTH_SHORT).show();
}
}, throwable -> {
Toast.makeText(PhotoActivity.this, "保存图片失败", Toast.LENGTH_SHORT).show();
Log.d(TAG, throwable.getMessage());
throwable.printStackTrace();
});
}
@Override
public void onBitmapFailed(Drawable errorDrawable) {
Toast.makeText(PhotoActivity.this, "保存图片失败", Toast.LENGTH_SHORT).show();
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
}
};
Picasso.with(this)
.load(url)
.into(target);
}
我们来看一下downloadImage方法
这里Picasso的into方法中也用了Target,具体原因刚才介绍过了,为了获得Bitmap对象。拿到bitmap之后接下来就是要保存到本地,考虑到图片可能很大,保存操作可能是个耗时操作,为了提升界面流畅度,我们需要在线程中进行。这里我们用RxJava来进行异步操作。RxJava教程可以参考这里。RxJava有一个很好的地方就是可以很方便的切换线程,这里我们让图片保存的操作在Schedulers.io()线程也就是IO线程中进行,回调显示在AndroidSchedulers.mainThread()也就是android的主线程中进行,这样,就可以很方便的进行耗时操作并显示结果了。我们可以把线程打印出来
02-21 14:12:44.976 29015-29074/me.masteryi.gankio D/PhotoActivity: thread1:RxCachedThreadScheduler-2
02-21 14:12:45.116 29015-29015/me.masteryi.gankio D/PhotoActivity: thread2:main
可以看出来保存图片的操作发生在RxCachedThreadScheduler这个线程,而显示结果的线程发生在main线程,也就是主线程。
关于subscribeOn和observeOn这两个方法我也看了很久,根据官方文档
To specify on which Scheduler the Observable should invoke its observers’ onNext, onCompleted, and onError methods, use the observeOn operator, passing it the appropriate Scheduler.
To specify on which Scheduler the Observable should operate, use the subscribeOn operator, passing it the appropriate Scheduler.
我的理解是subscribeOn指定Observable所运行的线程,也就是这里我们Observable.creat()运行的线程,obserOn指定onNext,onError等方法运行的线程,所以我们可以在create中运行耗时方法,在onNext中调用改变UI的方法。我不知道这样理解有没有问题,如果有错,希望大家能够指出来。
好了,最基本的功能都已经实现了,后面我们会加入一些扩展的功能。