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基础不好的小水怪,正在学习。有错请指出,一起加油。