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的源码分析以及使用请移步下个文章

PhotoView的源码分析以及使用

Java基础不好的小水怪,正在学习。有错请指出,一起加油。

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