项目中遇到需要一款能够点击浏览指定图片的控件,在网上搜索之后没有发现能完全达到需求的控件。因此决定定制一款适合的控件,本文用来分享制作的过程和成果的展示。
点击banner图,进入图片浏览模式; 双击放大图片,向上,向下滑动可退出; 点击保存按钮可以保存图片;
1.搭建界面 ```xml <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layoutwidth="matchparent" android:layoutheight="matchparent">
<com.ishuangniu.customeview.picturepreview.image.FloatViewPager android:id="@+id/rlroot" android:layoutwidth="match_parent" android:layoutheight="matchparent" 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" />
```
界面中以ViewPager展示滑动页面。 页面将搭配Fragment实现展示不同的图片,每个Fragment展示一张图片
2.图片数据
在项目中,装载图片数据的实体未必都是String类型;因此,此处封装的控件,传递的数据统一实现接口;
定义图片接口ImageSource
ImageSource定义了图片的地址,实体类实现imageUrl()方法,控件使用该方法得到图片的地址。代码如下
```java public interface ImageSource extends Serializable {
String imageUrl();
} ``` 3.图片放大手势
PinchImageView地址 https://github.com/boycy815/PinchImageView 图片放大操作使用开源代码PinchImageView实现,该开源代码就View类,实现了手势缩放,双击放大等操作。代码由国内的人员书写,适合普通手势操作的需要;
4.滑动退出手势
在界面中使用了FloatViewPager这个封装的ViewPager控件;将滑动退出的手势封装在了控件中,将手势操作解耦; 手势操作部分关键代码如下
```java @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.ACTIONDOWN: 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.ACTIONMOVE: 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接口 ```java public class OkHttpFetcher implements DataFetcher{
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接口 ```java 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
java 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是网络加载监听器,用来监听图片下载的进度;代码如下 ```java 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封装了下载监听的逻辑
```java 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文件当中加入如下配置xml <manifest> ... <application> <meta-data android:name="com.example.glideprogresstest.MyGlideModule" android:value="GlideModule" /> ... </application> </manifest>
为了方便使用,写了一个工具类,使用工具类进行调用图片浏览器 使用代码代码如下:java ImagePreviousTools.with(mContext) .setArrayList(goodsImgBeanList) .setImageLoader(ImageLoaderImpl.getInstance()) .show();
文章中出现的代码展示不全,知识粘贴了部分核心代码。项目代码可以在android双牛掌柜源代码中查看。
技术总结:济宁远之航网络科技技术二部李志强
All Rights Reserved © 2014-2018 鲁ICP备14000487号 | 营业执照 | 增值电信业务许可证 鲁B2-20190121 | 山东远之航信息技术有限公司 |