自定義圖片瀏覽器

引言


項目中遇到需要一款能夠點擊瀏覽指定圖片的控件,在網上搜索之後沒有發現能完全達到需求的控件。因此決定定製一款適合的控件,本文用來分享製作的過程和成果的展示。

效果展示


點擊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雙牛掌櫃源代碼中查看。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章