引言
项目中遇到需要一款能够点击浏览指定图片的控件,在网上搜索之后没有发现能完全达到需求的控件。因此决定定制一款适合的控件,本文用来分享制作的过程和成果的展示。
效果展示
image
点击banner图,进入图片浏览模式;
双击放大图片,向上,向下滑动可退出;
点击保存按钮可以保存图片;
实现
1.搭建界面
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.ishuangniu.customeview.picturepreview.image.FloatViewPager
android:id="@+id/rl_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000" />
<ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/tv_page"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_margin="20dp"
android:text="0/0"
android:textColor="#fff"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_download"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_margin="15dp"
android:text="保存"
android:textColor="#fff"
android:textSize="16sp" />
</RelativeLayout>
界面中以ViewPager展示滑动页面。
页面将搭配Fragment实现展示不同的图片,每个Fragment展示一张图片
2.图片数据
在项目中,装载图片数据的实体未必都是String类型;因此,此处封装的控件,传递的数据统一实现接口;
定义图片接口ImageSource
ImageSource定义了图片的地址,实体类实现imageUrl()方法,控件使用该方法得到图片的地址。代码如下
public interface ImageSource extends Serializable {
String imageUrl();
}
3.图片放大手势
PinchImageView地址 https://github.com/boycy815/PinchImageView
图片放大操作使用开源代码PinchImageView实现,该开源代码就View类,实现了手势缩放,双击放大等操作。代码由国内的人员书写,适合普通手势操作的需要;
4.滑动退出手势
在界面中使用了FloatViewPager这个封装的ViewPager控件;将滑动退出的手势封装在了控件中,将手势操作解耦;
手势操作部分关键代码如下
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(TAG, "dispatchTouchEvent()" + ev);
if (mFlinging || mScrolling) {
Log.d(TAG, "not need handle event when view is anim");
return true;
}
if (mDisallowInterruptHandler != null && mDisallowInterruptHandler.disallowInterrupt()) {
Log.d(TAG, "disallow interrupt,just handle by super");
return super.dispatchTouchEvent(ev);
}
int actionMask = ev.getActionMasked();
Log.d(TAG, "actionMask=" + actionMask + "mTouchState=" + mTouchState);
switch (actionMask) {
case MotionEvent.ACTION_DOWN:
mTouchState = TouchState.NONE;
mLastMotionX = ev.getRawX();
mLastMotionY = ev.getRawY();
mLastDownX = ev.getRawX();
mLastDownY = ev.getRawY();
Log.d(TAG, "mLastMotionX=" + mLastMotionX);
Log.d(TAG, "ev.getRawX()=" + ev.getRawX());
Log.d(TAG, "mLastMotionY=" + mLastMotionY);
break;
case MotionEvent.ACTION_MOVE:
final float x = ev.getRawX();
final float xDistance = Math.abs(x - mLastDownX);
final float y = ev.getRawY();
final float yDistance = Math.abs(y - mLastDownY);
Log.d(TAG, "ev.getRawX()=" + x);
Log.d(TAG, "mLastMotionX=" + mLastMotionX);
Log.d(TAG, "ev.getRawY()=" + y);
Log.d(TAG, "mLastMotionY=" + mLastMotionY);
Log.d(TAG, "xDistance=" + xDistance + "yDistance=" + yDistance + "mTouchSlop=" + mTouchSlop);
//判断触摸方向
if (mTouchState == TouchState.NONE) {
if (xDistance + mTouchSlop < yDistance) {
mTouchState = TouchState.VERTICAL_MOVE;
}
if (xDistance > yDistance + mTouchSlop) {
mTouchState = TouchState.HORIZONTAL_MOVE;
}
}
//如果是纵向触摸,移动ViewPager
if (mTouchState == TouchState.VERTICAL_MOVE) {
move(false, x - mLastMotionX, (y - mLastMotionY));
}
mLastMotionX = ev.getRawX();
mLastMotionY = ev.getRawY();
break;
case MotionEvent.ACTION_UP:
mLastMotionX = ev.getRawX();
mLastMotionY = ev.getRawY();
//纵向触摸结束,判断是否需要飞出,需要ViewPager动画飞出,不需要,飞回原位
if (mTouchState == TouchState.VERTICAL_MOVE) {
if (needToFlingOut()) {
int finalY = getTop() < mInitTop ? -(mHeight + mInitTop) : mParent.getHeight();
mFlinging = true;
startScrollTopView(0, finalY, FLING_OUT_DURATION);
} else {
startScrollTopView(mInitLeft, mInitTop, SCROLL_BACK_DURATION);
}
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
if (mTouchState != TouchState.VERTICAL_MOVE) {
mTouchState = TouchState.MORE_TOUCH;
}
break;
default:
break;
}
//除了纵向触摸,其他都由父类的super.dispatchTouchEvent(ev)处理
if (mTouchState == TouchState.VERTICAL_MOVE) {
return true;
} else {
Log.d(TAG, "super.dispatchTouchEvent()");
return super.dispatchTouchEvent(ev);
}
}
5.实现图片加载的进度动画
监听动画是为了提高UI交互的人性化,小图直接加载,进度条的优势展现不出来,当展示大图的时候,在加载的过程会出现一段时间的空白期,严重影响用户体验;
因此需要要使用加载动画,展示加载进度。
控件加载使用的第三方控件Glide。Glide功能很强大,但是每中不足的是不能监听加载进度。因此需要一些操作实现监听加载进度;
监听加载进度,实际上就是监听Glide网络加载的进度,GLide有自己的网络加载方式,但是没有暴露出来,无法被开发者监听,因此需要替换Glide自带的网络加载方式。替换原理百度一下就可以,此处只介绍过程;以下根据郭神的开源文章整理。
(1)新建OkHttpFetcher类,实现DataFetcher接口
public class OkHttpFetcher implements DataFetcher<InputStream> {
private final OkHttpClient client;
private final GlideUrl url;
private InputStream stream;
private ResponseBody responseBody;
private volatile boolean isCancelled;
public OkHttpFetcher(OkHttpClient client, GlideUrl url) {
this.client = client;
this.url = url;
}
@Override
public InputStream loadData(Priority priority) throws Exception {
Request.Builder requestBuilder = new Request.Builder()
.url(url.toStringUrl());
for (Map.Entry<String, String> headerEntry : url.getHeaders().entrySet()) {
String key = headerEntry.getKey();
requestBuilder.addHeader(key, headerEntry.getValue());
}
Request request = requestBuilder.build();
if (isCancelled) {
return null;
}
Response response = client.newCall(request).execute();
responseBody = response.body();
if (!response.isSuccessful() || responseBody == null) {
throw new IOException("Request failed with code: " + response.code());
}
stream = ContentLengthInputStream.obtain(responseBody.byteStream(),
responseBody.contentLength());
return stream;
}
@Override
public void cleanup() {
try {
if (stream != null) {
stream.close();
}
if (responseBody != null) {
responseBody.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public String getId() {
return url.getCacheKey();
}
@Override
public void cancel() {
isCancelled = true;
}
}
(2)新建OkHttpGlideUrlLoader类,并且实现ModelLoader接口
public class OkHttpGlideUrlLoader implements ModelLoader<GlideUrl, InputStream> {
private OkHttpClient okHttpClient;
public static class Factory implements ModelLoaderFactory<GlideUrl, InputStream> {
private OkHttpClient client;
public Factory() {
}
public Factory(OkHttpClient client) {
this.client = client;
}
private synchronized OkHttpClient getOkHttpClient() {
if (client == null) {
client = new OkHttpClient();
}
return client;
}
@Override
public ModelLoader<GlideUrl, InputStream> build(Context context, GenericLoaderFactory factories) {
return new OkHttpGlideUrlLoader(getOkHttpClient());
}
@Override
public void teardown() {
}
}
public OkHttpGlideUrlLoader(OkHttpClient client) {
this.okHttpClient = client;
}
@Override
public DataFetcher<InputStream> getResourceFetcher(GlideUrl model, int width, int height) {
return new OkHttpFetcher(okHttpClient, model);
}
}
(3)新建并替换GlideModule
public class MyGlideModule implements GlideModule {
...
@Override
public void registerComponents(Context context, Glide glide) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.addInterceptor(new ProgressInterceptor());
OkHttpClient okHttpClient = builder.build();
glide.register(GlideUrl.class, InputStream.class, new OkHttpGlideUrlLoader.Factory(okHttpClient));
}
}
其中ProgressInterceptor是网络加载监听器,用来监听图片下载的进度;代码如下
public class ProgressInterceptor implements Interceptor {
...
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
String url = request.url().toString();
ResponseBody body = response.body();
Response newResponse = response.newBuilder().body(new ProgressResponseBody(url, body)).build();
return newResponse;
}
}
ProgressResponseBody封装了下载监听的逻辑
public class ProgressResponseBody extends ResponseBody {
private static final String TAG = "ProgressResponseBody";
private BufferedSource bufferedSource;
private ResponseBody responseBody;
private ProgressListener listener;
public ProgressResponseBody(String url, ResponseBody responseBody) {
this.responseBody = responseBody;
listener = ProgressInterceptor.LISTENER_MAP.get(url);
}
@Override
public MediaType contentType() {
return responseBody.contentType();
}
@Override
public long contentLength() {
return responseBody.contentLength();
}
@Override
public BufferedSource source() {
if (bufferedSource == null) {
bufferedSource = Okio.buffer(new ProgressSource(responseBody.source()));
}
return bufferedSource;
}
private class ProgressSource extends ForwardingSource {
long totalBytesRead = 0;
int currentProgress;
ProgressSource(Source source) {
super(source);
}
@Override
public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
long fullLength = responseBody.contentLength();
if (bytesRead == -1) {
totalBytesRead = fullLength;
} else {
totalBytesRead += bytesRead;
}
int progress = (int) (100f * totalBytesRead / fullLength);
Log.d(TAG, "download progress is " + progress);
if (listener != null && progress != currentProgress) {
listener.onProgress(progress);
}
if (listener != null && totalBytesRead == fullLength) {
listener = null;
}
currentProgress = progress;
return bytesRead;
}
}
}
最后在AndroidManifest.xml文件当中加入如下配置
<manifest>
...
<application>
<meta-data
android:name="com.example.glideprogresstest.MyGlideModule"
android:value="GlideModule" />
...
</application>
</manifest>
使用
为了方便使用,写了一个工具类,使用工具类进行调用图片浏览器
使用代码代码如下:
ImagePreviousTools.with(mContext)
.setArrayList(goodsImgBeanList)
.setImageLoader(ImageLoaderImpl.getInstance())
.show();
说明
文章中出现的代码展示不全,知识粘贴了部分核心代码。项目代码可以在android双牛掌柜源代码中查看。
网友评论