引言
項目中遇到需要一款能夠點擊瀏覽指定圖片的控件,在網上搜索之後沒有發現能完全達到需求的控件。因此決定定製一款適合的控件,本文用來分享製作的過程和成果的展示。
效果展示
點擊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雙牛掌櫃源代碼中查看。