PhotoView框架的使用
一、背景
- 收到了設計小姐姐的一張設計圖,如下所示
- 需求分析,一個可以橫縱向四個方向滾動的列表
- 注意:文章穿插太多代碼,最好目錄跳轉查看。文字部分爲主要思想,代碼可略過。
二、開始敲代碼
方案1 自定義粗糙辣雞兒View
- 不考慮橫縱向都可以滾動的要求的話,這個圖一看就像是一個RecyclerView,然後通過LayoutManager(GridManager,表格佈局)控制其佈局的顯示方式。
- 接着考慮滾動的問題,RecycleView自己可以滾動,假如設置爲縱向滾動,那麼我們需要在RecyclerView監測到橫向滾動的時候攔截事件。
- 實現方式:使用RelativeLayout 嵌套一個RecyclerView 。根據事件分發機制,當監測到事件時,任務首先層層向下傳遞,沒人攔截,就傳給最底層View。此時監測到橫向滾動的時候,我們在父佈局中onInterceptTouchEvent攔截事件,返回true,不再向下傳遞。由父佈局直接處理。
- 問題:基礎效果實現了,但是用戶體驗度會非常差
1.如此實驗的界面,沒有考慮到慣性滑動,用戶滑動多少距離就移動多少距離,會覺得很卡頓;
解決辦法:手勢識別的onfling方法中進行處理,可以參考PhotoView 解析一文
2、同一時間只能橫向移動或縱向移動,必須等一個行爲停止之後,另一個纔會被響應;
package com.snap.awesomeserial.ui.widget;
public class FullInformationView extends RelativeLayout {
/**
* 手指按下時的位置
*/
private float mStartX = 0;
/**
* 滑動時和按下時的差值
*/
private float mMoveOffsetX = 0;
/**
* 展示數據時使用的RecycleView
*/
private RecyclerView mRecyclerView;
/**
* RecycleView的Adapter
*/
private FullInformationAdapter mAdapter;
private Context context;
/**
* 觸發攔截手勢的最小值
*/
private int mTriggerMoveDis = 30;
private float currentOffsetX;
private ScrollListener horizontalListener;
private ScrollListener verticalListener;
public FullInformationView(Context context) {
this(context, null);
}
public FullInformationView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FullInformationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
}
private void initView() {
LinearLayout linearLayout = new LinearLayout(getContext());
linearLayout.setOrientation(LinearLayout.VERTICAL);
linearLayout.addView(createMoveRecyclerView());
addView(linearLayout, new LayoutParams(LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
}
public void setHorizontalListener(ScrollListener horizontalListener) {
this.horizontalListener = horizontalListener;
}
public void setVerticalListener(ScrollListener verticalListener) {
this.verticalListener = verticalListener;
}
/**
* 創建數據展示佈局
**/
private View createMoveRecyclerView() {
FrameLayout linearLayout = new FrameLayout(getContext());
mRecyclerView = new RecyclerView(getContext());
GridLayoutManager layoutManager = new GridLayoutManager(context, 12, GridLayoutManager.VERTICAL, false);
mRecyclerView.setLayoutManager(layoutManager);
if (null != mAdapter) {
mRecyclerView.setAdapter(mAdapter);
}
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
//顯示區域的高度。
int extent = mRecyclerView.computeVerticalScrollExtent();
//整體的高度,注意是整體,包括在顯示區域之外的。
int range = mRecyclerView.computeVerticalScrollRange();
//已經向下滾動的距離,爲0時表示已處於頂部。
int offset = mRecyclerView.computeVerticalScrollOffset();
float percent = offset / ((range - extent) * 1f);
verticalListener.onScroll(percent);
}
});
linearLayout.addView(mRecyclerView, new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
return linearLayout;
}
/**
* 設置adapter
*
* @param adapter
*/
public void setAdapter(FullInformationAdapter adapter) {
mAdapter = adapter;
initView();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mStartX = ev.getX();
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_MOVE:
int offsetX = (int) Math.abs(ev.getX() - mStartX);
//水平移動大於30觸發攔截
if (offsetX > mTriggerMoveDis) {
return true;
} else {
return false;
}
default:
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float totalX = (getWidth() - AutoSizeUtils.dp2px(context, 1740));
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
return true;
case MotionEvent.ACTION_UP:
currentOffsetX = (mStartX - event.getX()) + currentOffsetX;
if (currentOffsetX > totalX) {
currentOffsetX = totalX;
} else if (currentOffsetX < 0) {
currentOffsetX = 0;
}
horizontalListener.onScroll(currentOffsetX / totalX);
break;
case MotionEvent.ACTION_MOVE:
//計算偏移位置[絕對值]
float offsetX = Math.abs(event.getX() - mStartX);
if (offsetX > mTriggerMoveDis) {
mMoveOffsetX = (mStartX - event.getX());
//當滑動大於最大寬度時,不在滑動(右邊到頭了)
float totalOffset = mMoveOffsetX + currentOffsetX;
if (totalOffset > totalX) {
totalOffset = totalX;
} else if (totalOffset < 0) {
totalOffset = 0;
}
//跟隨手指向右滾動
scrollTo((int) totalOffset, 0);
}
break;
default:
}
return super.onTouchEvent(event);
}
}
方案2 HorizontalScrollView嵌套RecyclerView
- 解決問題:慣性滑動
- 實現方式: HorizontalScrollView嵌套RecyclerView
- 結論,方案1中的做法,已有成熟的控件實現,且不會出現衝突。這就說明,一開始做需求分析的時候,就很有問題。浪費了很多時間。當然雖然浪費時間,實現效果也不理想,但是自己寫的過程中也加深了對事件分發的理解。
- 缺點:問題2,同一時間只能橫向移動或縱向移動,必須等一個行爲停止之後,另一個纔會被響應的問題依然存在。
如下所示,直接這樣寫就實現了橫縱向滑動的目的。
<HorizontalScrollView
android:id="@+id/horizontalView"
android:layout_width="1740dp"
android:layout_height="822dp"
android:orientation="horizontal"
android:overScrollMode="never"
android:scrollbars="none"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title_tv">
<android.support.v7.widget.RecyclerView
android:id="@+id/verticalRecyclerView"
android:layout_width="3324dp"
android:layout_height="match_parent"
android:overScrollMode="never" />
</HorizontalScrollView>
發現問題:縱向滑動不敏感。
解決辦法:重寫onInterceptTouchEvent方法,水平移動距離過小時,不攔截事件,傳遞給RecylerView縱向處理。治標不治本。辣雞兒處理方式。
public class HorizontalView extends HorizontalScrollView {
private int mTriggerMoveDis = 30;
private float mStartX;
public HorizontalView(Context context) {
super(context);
}
public HorizontalView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public HorizontalView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mStartX = ev.getX();
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_MOVE:
int offsetX = (int) Math.abs(ev.getX() - mStartX);
//水平移動大於30觸發攔截
if (offsetX > mTriggerMoveDis) {
return true;
} else {
return false;
}
default:
}
return super.onInterceptTouchEvent(ev);
}
}
監聽橫縱滾動進度
verticalRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
//顯示區域的高度。
int extent = verticalRecyclerView.computeVerticalScrollExtent();
//整體的高度,注意是整體,包括在顯示區域之外的。
int range = verticalRecyclerView.computeVerticalScrollRange();
//已經向下滾動的距離,爲0時表示已處於頂部。
int offset = verticalRecyclerView.computeVerticalScrollOffset();
float percent = offset / ((range - extent) * 1f);
verticalProgressBar.setProcess(percent);
}
});
horizontalView.setOnScrollChangeListener(new View.OnScrollChangeListener() {
@Override
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
horizontalProgressBar.setProcess(scrollX / (AutoSizeUtils.dp2px(FullInformationActivity.this, 1584) * 1f));
}
});
方案3 PhotoView
- 解決問題:同一時間只能橫向移動或縱向移動,必須等一個行爲停止之後,另一個纔會被響應。
- 解決方式:使用第三方框架PhotoView
1.自定義View畫出界面;
2.作爲一個Drawable設置給PhotoView控件;
3.通過縮放設置,實現圖片的橫縱向移動瀏覽功能;
4.設置監聽setOnMatrixChangeListener,實現滾動進度條; - 總結:自定義view應該直接加載xml的,這樣寫看着很亂;
什麼時候我能自己整個實現這個功能,把方案1補充完整,而不是用人家的輪子,就厲害啦。
1.自定義View畫出界面;
public class FullInformationView extends LinearLayout {
public FullInformationView(Context context) {
super(context);
setOrientation(VERTICAL);
}
public FullInformationView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
setOrientation(VERTICAL);
}
public void init(List<Sample> samples) {
int column = samples.size() / 12;
for (int i = 0; i < column; i++) {
LinearLayout row = new LinearLayout(getContext());
LayoutParams rowLayoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, ShadowContainer.LayoutParams.WRAP_CONTENT);
row.setOrientation(HORIZONTAL);
for (int j = 0; j < 12; j++) {
row.addView(getItem(samples.get(i * 12 + j)));
}
addView(row, rowLayoutParams);
}
}
private ConstraintLayout getItem(Sample sample) {
ConstraintLayout layout = new ConstraintLayout(getContext());
layout.setId(R.id.full_information_item);
LayoutParams layoutParams = new LayoutParams(dp2px(264), dp2px(210));
layoutParams.setMargins(dp2px(12), dp2px(12), dp2px(12), dp2px(12));
layout.setPadding(0, dp2px(28), 0, 0);
layout.setBackground(getContext().getDrawable(R.drawable.bg_full_information_item));
layout.setLayoutParams(layoutParams);
TextView holeTv = new TextView(getContext());
holeTv.setId(R.id.hole_tv);
holeTv.setTextSize(24);
holeTv.setTextColor(0xff333333);
holeTv.setGravity(Gravity.CENTER);
holeTv.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
ConstraintLayout.LayoutParams holeTvLp = new ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.WRAP_CONTENT, ConstraintLayout.LayoutParams.WRAP_CONTENT);
holeTvLp.setMargins(dp2px(24), 0, dp2px(24), 0);
holeTvLp.startToStart = layout.getId();
holeTvLp.topToTop = layout.getId();
layout.addView(holeTv, holeTvLp);
TextView standardTv = new TextView(getContext());
standardTv.setBackground(getContext().getDrawable(R.drawable.ic_standard_none));
standardTv.setTextSize(24);
standardTv.setTextColor(getContext().getColor(R.color.white));
standardTv.setGravity(Gravity.CENTER);
ConstraintLayout.LayoutParams standardTvLp = new ConstraintLayout.LayoutParams(dp2px(36), dp2px(36));
standardTvLp.endToEnd = layout.getId();
standardTvLp.topToTop = layout.getId();
layout.addView(standardTv, standardTvLp);
TextView famProbeTv = new TextView(getContext());
famProbeTv.setId(R.id.fam_probe_tv);
famProbeTv.setTextSize(24);
famProbeTv.setTextColor(0xff666666);
famProbeTv.setGravity(Gravity.CENTER_VERTICAL);
famProbeTv.setCompoundDrawablesWithIntrinsicBounds(getContext().getDrawable(R.drawable.oval_probe_ch1_12), null, null, null);
famProbeTv.setCompoundDrawablePadding(dp2px(12));
ConstraintLayout.LayoutParams famProbeTvLp = new ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.WRAP_CONTENT, ConstraintLayout.LayoutParams.WRAP_CONTENT);
famProbeTvLp.setMargins(dp2px(24), dp2px(18), 0, 0);
famProbeTvLp.startToStart = layout.getId();
famProbeTvLp.topToBottom = holeTv.getId();
layout.addView(famProbeTv, famProbeTvLp);
TextView vicProbeTv = new TextView(getContext());
vicProbeTv.setId(R.id.vic_probe_tv);
vicProbeTv.setTextSize(24);
vicProbeTv.setTextColor(0xff666666);
vicProbeTv.setGravity(Gravity.CENTER_VERTICAL);
vicProbeTv.setCompoundDrawablesWithIntrinsicBounds(getContext().getDrawable(R.drawable.oval_probe_ch2_12), null, null, null);
vicProbeTv.setCompoundDrawablePadding(dp2px(12));
ConstraintLayout.LayoutParams vicProbeTvLp = new ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.WRAP_CONTENT, ConstraintLayout.LayoutParams.WRAP_CONTENT);
vicProbeTvLp.setMargins(dp2px(132), dp2px(18), 0, 0);
vicProbeTvLp.startToStart = layout.getId();
vicProbeTvLp.topToBottom = holeTv.getId();
layout.addView(vicProbeTv, vicProbeTvLp);
TextView roxProbeTv = new TextView(getContext());
roxProbeTv.setId(R.id.rox_probe_tv);
roxProbeTv.setTextSize(24);
roxProbeTv.setTextColor(0xff666666);
roxProbeTv.setGravity(Gravity.CENTER_VERTICAL);
roxProbeTv.setCompoundDrawablesWithIntrinsicBounds(getContext().getDrawable(R.drawable.oval_probe_ch3_12), null, null, null);
roxProbeTv.setCompoundDrawablePadding(dp2px(12));
ConstraintLayout.LayoutParams roxProbeTvLp = new ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.WRAP_CONTENT, ConstraintLayout.LayoutParams.WRAP_CONTENT);
roxProbeTvLp.setMargins(0, dp2px(12), 0, 0);
roxProbeTvLp.startToStart = famProbeTv.getId();
roxProbeTvLp.topToBottom = famProbeTv.getId();
layout.addView(roxProbeTv, roxProbeTvLp);
TextView cy5ProbeTv = new TextView(getContext());
cy5ProbeTv.setId(R.id.cy5_probe_tv);
cy5ProbeTv.setTextSize(24);
cy5ProbeTv.setTextColor(0xff666666);
cy5ProbeTv.setGravity(Gravity.CENTER_VERTICAL);
cy5ProbeTv.setCompoundDrawablesWithIntrinsicBounds(getContext().getDrawable(R.drawable.oval_probe_ch4_12), null, null, null);
cy5ProbeTv.setCompoundDrawablePadding(dp2px(12));
ConstraintLayout.LayoutParams cy5ProbeTvLp = new ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.WRAP_CONTENT, ConstraintLayout.LayoutParams.WRAP_CONTENT);
cy5ProbeTvLp.setMargins(0, dp2px(12), 0, 0);
cy5ProbeTvLp.startToStart = vicProbeTv.getId();
cy5ProbeTvLp.topToBottom = famProbeTv.getId();
layout.addView(cy5ProbeTv, cy5ProbeTvLp);
TextView sampleNameTv = new TextView(getContext());
sampleNameTv.setBackgroundColor(getContext().getColor(R.color.white));
sampleNameTv.setId(R.id.cy5_probe_tv);
sampleNameTv.setTextSize(21);
sampleNameTv.setTextColor(0xffe5e5e5);
sampleNameTv.setGravity(Gravity.CENTER);
ConstraintLayout.LayoutParams sampleNameTvLp = new ConstraintLayout.LayoutParams(dp2px(100), ConstraintLayout.LayoutParams.WRAP_CONTENT);
sampleNameTvLp.setMargins(0, 0, 0, dp2px(24));
sampleNameTvLp.bottomToBottom = layout.getId();
sampleNameTvLp.endToEnd = layout.getId();
sampleNameTvLp.startToStart = layout.getId();
View view = new View(getContext());
view.setBackgroundColor(0xFFE5E5E5);
ConstraintLayout.LayoutParams viewLp = new ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.MATCH_PARENT, dp2px(3));
viewLp.setMargins(dp2px(24), 0, dp2px(24), 0);
viewLp.bottomToBottom = sampleNameTv.getId();
viewLp.topToTop = sampleNameTv.getId();
layout.addView(view, viewLp);
layout.addView(sampleNameTv, sampleNameTvLp);
//設置數據
holeTv.setText("A" + sample.getIndex());
setProbe(famProbeTv, vicProbeTv, roxProbeTv, cy5ProbeTv, sample);
setStandard(standardTv, sample);
sampleNameTv.setText(sample.getName() == null ? "N/A" : sample.getName());
return layout;
}
private void setProbe(TextView famProbeTv, TextView vicProbeTv, TextView roxProbeTv, TextView cy5ProbeTv, Sample sample) {
famProbeTv.setText(sample.getCh1Probe() == null ? "N/A" : sample.getCh1Probe());
vicProbeTv.setText(sample.getCh2Probe() == null ? "N/A" : sample.getCh2Probe());
roxProbeTv.setText(sample.getCh3Probe() == null ? "N/A" : sample.getCh3Probe());
cy5ProbeTv.setText(sample.getCh4Probe() == null ? "N/A" : sample.getCh4Probe());
}
private void setStandard(TextView standardTv, Sample sample) {
String tag = null;
if (sample.getTag() == 0) {
standardTv.setVisibility(View.GONE);
} else {
standardTv.setVisibility(View.VISIBLE);
if (sample.getTag() == Constants.SAMPLE_TAG_UNKNOWN) {
tag = "U";
} else if (sample.getTag() == Constants.SAMPLE_TAG_STANDARD) {
tag = "S";
} else if (sample.getTag() == Constants.SAMPLE_TAG_POSITIVE) {
tag = "P";
} else if (sample.getTag() == Constants.SAMPLE_TAG_NEGATIVE) {
tag = "N";
}
standardTv.setText(tag);
}
}
private int dp2px(int value) {
return AutoSizeUtils.dp2px(getContext(), value);
}
}
2.作爲一個Drawable設置給PhotoView控件;
3.通過縮放設置,實現圖片的橫縱向移動瀏覽功能;
full_information_view.post(new Runnable() {
@Override
public void run() {
Bitmap bitmap = loadBitmapFromView(full_information_view);
photoView.setImageBitmap(bitmap);
Matrix matrix = new Matrix();
matrix.setScale(2f, 2f,0,0);
photoView.setDisplayMatrix(matrix);
}
});
public static Bitmap loadBitmapFromView(FullInformationView v) {
Bitmap b = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
c.drawColor(Color.WHITE);
v.layout(0, 0, v.getLayoutParams().width, v.getLayoutParams().height);
v.draw(c);
return b;
}
4.設置監聽setOnMatrixChangeListener,實現滾動進度條;
photoView.setOnMatrixChangeListener(new OnMatrixChangedListener() {
@Override
public void onMatrixChanged(RectF rect) {
Matrix matrix = new Matrix();
photoView.getDisplayMatrix(matrix);
float[] floats = new float[9];
matrix.getValues(floats);
float scaleX = floats[0];
float scaleY = floats[4];
float offsetX =Math.abs(floats[2]) ;
float offsetY = Math.abs(floats[5]);
}
});
PhotoView的源碼分析以及使用請移步下個文章
Java基礎不好的小水怪,正在學習。有錯請指出,一起加油。