自定义图片浏览器

2018-05-20  本文已影响0人  时间在走

引言


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

效果展示


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

上一篇 下一篇

猜你喜欢

热点阅读