「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」
一、背景
- 收到了设计小姐姐的一张设计图,如下所示
- 需求分析,一个可以横纵向四个方向滚动的列表
- 注意:文章穿插太多代码,最好目录跳转查看。文字部分为主要思想,代码可略过。
二、开始敲代码
方案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]); } });