官方微信公众平台

Android下自定义图片浏览器


发布时间:2018-08-03
论文

引言


项目中遇到需要一款能够点击浏览指定图片的控件,在网上搜索之后没有发现能完全达到需求的控件。因此决定定制一款适合的控件,本文用来分享制作的过程和成果的展示。

效果展示


image

点击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双牛掌柜源代码中查看。


技术总结:济宁远之航网络科技技术二部李志强


  • 专注于服务电商领域

  • 资深电商服务专家

  • 一对一专属会员经理

  • 365万用户诚意推荐

业务QQ:1014905292

业务电话:13053758877

客服电话:400-032-5039

服务时间:08:00 — 18:00

扫一扫了解详情

All Rights Reserved © 2014-2019 鲁ICP备14000487号-4 山东远之航信息技术有限公司 |

鲁公网安备 37081102000368号