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基礎不好的小水怪,正在學習。有錯請指出,一起加油。

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