XImageView-RatioImageView按比例展示ImageView

zhuxh發表於2019-01-19

RatioImageView實現ImageView按比例縮放等效果

0. 原始碼地址

github.com/zhxhcoder/X…

1. 引用方法

compile 'com.zhxh:ximageviewlib:1.2'
複製程式碼

2. 使用方法

舉個例子:

        <com.zhxh.ximageviewlib.RatioImageView
            android:id="@+id/ad_app_image"
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:scaleType="centerCrop"
            android:src="@drawable/ic_test_750_360"
            app:riv_height_to_width_ratio="0.48"
            tools:ignore="ContentDescription" />
複製程式碼

上面riv_height_to_width_ratio=0.48 已經定義layout_width="100dp" 計算得出 layout_height="48dp"

實現效果

XImageView

3. 原始碼實現

3.1 屬性定義與描述

    <declare-styleable name="RatioImageView">
        <!-- 寬度是否根據src圖片的比例來測量(高度已知) -->
        <attr name="riv_is_width_fix_drawable_size_ratio" format="boolean" />
        <!-- 高度是否根據src圖片的比例來測量(寬度已知) -->
        <attr name="riv_is_height_fix_drawable_size_ratio" format="boolean" />
        <!--當mIsWidthFitDrawableSizeRatio生效時,最大寬度-->
        <attr name="riv_max_width_when_width_fix_drawable" format="dimension" />
        <!--當mIsHeightFitDrawableSizeRatio生效時-->
        <attr name="riv_max_height_when_height_fix_drawable" format="dimension" />
        <!-- 高度設定,參考寬度,如0.5 , 表示 高度=寬度×0.5 -->
        <attr name="riv_height_to_width_ratio" format="float" />
        <!-- 寬度設定,參考高度,如0.5 , 表示 寬度=高度×0.5 -->
        <attr name="riv_width_to_height_ratio" format="float" />
        <!--寬度和高度,避免layout_width/layout_height會在超過螢幕尺寸時特殊處理的情況-->
        <attr name="riv_width" format="dimension" />
        <attr name="riv_height" format="dimension" />
    </declare-styleable>
複製程式碼

3.2 程式碼實現

1,屬性初始化 從AttributeSet 中初始化相關屬性

  private void init(AttributeSet attrs) {
        TypedArray a = getContext().obtainStyledAttributes(attrs,
                R.styleable.RatioImageView);
        mIsWidthFitDrawableSizeRatio = a.getBoolean(R.styleable.RatioImageView_riv_is_width_fix_drawable_size_ratio,
                mIsWidthFitDrawableSizeRatio);
        mIsHeightFitDrawableSizeRatio = a.getBoolean(R.styleable.RatioImageView_riv_is_height_fix_drawable_size_ratio,
                mIsHeightFitDrawableSizeRatio);
        mMaxWidthWhenWidthFixDrawable = a.getDimensionPixelOffset(R.styleable.RatioImageView_riv_max_width_when_width_fix_drawable,
                mMaxWidthWhenWidthFixDrawable);
        mMaxHeightWhenHeightFixDrawable = a.getDimensionPixelOffset(R.styleable.RatioImageView_riv_max_height_when_height_fix_drawable,
                mMaxHeightWhenHeightFixDrawable);
        mHeightRatio = a.getFloat(
                R.styleable.RatioImageView_riv_height_to_width_ratio, mHeightRatio);
        mWidthRatio = a.getFloat(
                R.styleable.RatioImageView_riv_width_to_height_ratio, mWidthRatio);
        mDesiredWidth = a.getDimensionPixelOffset(R.styleable.RatioImageView_riv_width, mDesiredWidth);
        mDesiredHeight = a.getDimensionPixelOffset(R.styleable.RatioImageView_riv_height, mDesiredHeight);

        a.recycle();
    }
複製程式碼

2,關鍵資料初始化

mDrawableSizeRatio = -1f; // src圖片(前景圖)的寬高比例

在建構函式中呼叫以下程式碼,當mDrawable不為空時

            mDrawableSizeRatio = 1f * getDrawable().getIntrinsicWidth()
                    / getDrawable().getIntrinsicHeight();
複製程式碼

其他變數初始化

    private boolean mIsWidthFitDrawableSizeRatio; // 寬度是否根據src圖片(前景圖)的比例來測量(高度已知)
    private boolean mIsHeightFitDrawableSizeRatio; // 高度是否根據src圖片(前景圖)的比例來測量(寬度已知)
    private int mMaxWidthWhenWidthFixDrawable = -1; // 當mIsWidthFitDrawableSizeRatio生效時,最大寬度
    private int mMaxHeightWhenHeightFixDrawable = -1; // 當mIsHeightFitDrawableSizeRatio生效時,最大高度

    // 寬高比例
    private float mWidthRatio = -1; // 寬度 = 高度*mWidthRatio
    private float mHeightRatio = -1; // 高度 = 寬度*mHeightRatio

    private int mDesiredWidth = -1; // 寬度和高度,避免layout_width/layout_height會在超過螢幕尺寸時特殊處理的情況
    private int mDesiredHeight = -1;

複製程式碼

3,重新生成所需的drawable 我們覆蓋ImageView的setImageResource與setImageDrawable函式,對生成的drawable物件重新自定義

    @Override
    public void setImageResource(int resId) {
        super.setImageResource(resId);
        reSetDrawable();
    }

    @Override
    public void setImageDrawable(Drawable drawable) {
        super.setImageDrawable(drawable);
        reSetDrawable();
    }
複製程式碼

自定義所需的drawable物件

    private void reSetDrawable() {
        Drawable drawable = getDrawable();
        if (drawable != null) {
            // 發生變化,重新調整佈局
            if (mIsWidthFitDrawableSizeRatio || mIsHeightFitDrawableSizeRatio) {
                float old = mDrawableSizeRatio;
                mDrawableSizeRatio = 1f * drawable.getIntrinsicWidth()
                        / drawable.getIntrinsicHeight();
                if (old != mDrawableSizeRatio && mDrawableSizeRatio > 0) {
                    requestLayout();
                }
            }
        }
    }
複製程式碼

從上面程式碼我們看到,當圖片本身比例與定義比例不同時,會呼叫 **requestLayout();**方法重新調整佈局。 該方法的作用是什麼呢? 我們進入 **requestLayout()**方法:


    /**
     * Call this when something has changed which has invalidated the
     * layout of this view. This will schedule a layout pass of the view
     * tree. This should not be called while the view hierarchy is currently in a layout
     * pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the
     * end of the current layout pass (and then layout will run again) or after the current
     * frame is drawn and the next layout occurs.
     *
     * <p>Subclasses which override this method should call the superclass method to
     * handle possible request-during-layout errors correctly.</p>
     */
    @CallSuper
    public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();

        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            // Only trigger request-during-layout logic if this is the view requesting it,
            // not the views in its parent hierarchy
            ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }

        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }
複製程式碼

上面是Android view中該方法的定義,從程式碼中我們可以看出它首先先判斷當前View樹是否正在佈局流程,接著為當前子View設定標記位,該標記位的作用就是標記了當前的View是需要進行重新佈局的,接著呼叫mParent.requestLayout方法,這個十分重要,因為這裡是向父容器請求佈局,即呼叫父容器的requestLayout方法,為父容器新增PFLAG_FORCE_LAYOUT標記位,而父容器又會呼叫它的父容器的requestLayout方法,即requestLayout事件層層向上傳遞,直到DecorView,即根View,而根View又會傳遞給ViewRootImpl,也即是說子View的requestLayout事件,最終會被ViewRootImpl接收並得到處理。可以看出這種向上傳遞的流程,其實是採用了責任鏈模式,即不斷向上傳遞該事件,直到找到能處理該事件的上級,在這裡,只有ViewRootImpl能夠處理requestLayout事件。

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
複製程式碼

我們進一步深入,可以看出在ViewRootImpl中,重寫了requestLayout方法。 在這裡,呼叫了scheduleTraversals方法,這個方法是一個非同步方法,最終會呼叫到ViewRootImpl#performTraversals方法,這也是View工作流程的核心方法,在這個方法內部,分別呼叫measure、layout、draw方法來進行View的三大工作流程,對於三大工作流程,前幾篇文章已經詳細講述了,這裡再做一點補充說明。 先看View#measure方法:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
     ...

    if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
            widthMeasureSpec != mOldWidthMeasureSpec ||
            heightMeasureSpec != mOldHeightMeasureSpec) {
        ...省略無關程式碼...
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // measure ourselves, this should set the measured dimension flag back
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } 
        ...省略無關程式碼...
        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    }
}

複製程式碼

首先是判斷一下標記位,如果當前View的標記位為PFLAG_FORCE_LAYOUT,那麼就會進行測量流程,呼叫onMeasure,對該View進行測量,接著最後為標記位設定為PFLAG_LAYOUT_REQUIRED,這個標記位的作用就是在View的layout流程中,如果當前View設定了該標記位,則會進行佈局流程。具體可以看如下View#layout原始碼:

public void layout(int l, int t, int r, int b) {
     ...省略無關程式碼...
    //判斷標記位是否為PFLAG_LAYOUT_REQUIRED,如果有,則對該View進行佈局
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
        //onLayout方法完成後,清除PFLAG_LAYOUT_REQUIRED標記位
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }

    //最後清除PFLAG_FORCE_LAYOUT標記位
    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
複製程式碼

從上面的分析可以看出當子View呼叫requestLayout方法,會標記當前View及父容器,同時逐層向上提交,直到ViewRootImpl處理該事件,ViewRootImpl會呼叫三大流程,從measure開始,對於每一個含有標記位的view及其子View都會進行測量、佈局、繪製。

另外我也在這裡簡單介紹下當呼叫invalidate和postInvalidate時,View的內部呼叫邏輯。 直接上結論: 當子View呼叫了invalidate方法後,會為該View新增一個標記位,同時不斷向父容器請求重新整理,父容器通過計算得出自身需要重繪的區域,直到傳遞到ViewRootImpl中,最終觸發performTraversals方法,進行開始View樹重繪流程(只繪製需要重繪的檢視)。

View的生命週期

回到,XimageView中,我們知道,當呼叫requestLayout時會呼叫 onMeasure和onLayout以及onDraw函式,因為比例發生變化,我們需要重新測量,方法如下:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 優先順序從大到小:
        // mIsWidthFitDrawableSizeRatio mIsHeightFitDrawableSizeRatio
        // mWidthRatio mHeightRatio
        if (mDrawableSizeRatio > 0) {
            // 根據前景圖寬高比例來測量view的大小
            if (mIsWidthFitDrawableSizeRatio) {
                mWidthRatio = mDrawableSizeRatio;
            } else if (mIsHeightFitDrawableSizeRatio) {
                mHeightRatio = 1 / mDrawableSizeRatio;
            }
        }

        if (mHeightRatio > 0 && mWidthRatio > 0) {
            throw new RuntimeException("高度和寬度不能同時設定百分比!!");
        }

        if (mWidthRatio > 0) { // 高度已知,根據比例,設定寬度
            int height = 0;
            if (mDesiredHeight > 0) {
                height = mDesiredHeight;
            } else {
                height = MeasureSpec.getSize(heightMeasureSpec);
            }
            int width = (int) (height * mWidthRatio);
            if (mIsWidthFitDrawableSizeRatio && mMaxWidthWhenWidthFixDrawable > 0
                    && width > mMaxWidthWhenWidthFixDrawable) { // 限制最大寬度
                width = mMaxWidthWhenWidthFixDrawable;
                height = (int) (width / mWidthRatio);
            }
            super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
        } else if (mHeightRatio > 0) { // 寬度已知,根據比例,設定高度
            int width = 0;
            if (mDesiredWidth > 0) {
                width = mDesiredWidth;
            } else {
                width = MeasureSpec.getSize(widthMeasureSpec);
            }
            int height = (int) (width * mHeightRatio);
            if (mIsHeightFitDrawableSizeRatio && mMaxHeightWhenHeightFixDrawable > 0
                    && height > mMaxHeightWhenHeightFixDrawable) { // 限制最大高度
                height = mMaxHeightWhenHeightFixDrawable;
                width = (int) (height / mHeightRatio);
            }
            super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
        } else if (mDesiredHeight > 0 && mDesiredWidth > 0) { // 當沒有設定其他屬性時,width和height必須同時設定才生效
            int width = mDesiredWidth;
            int height = mDesiredHeight;
            super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
        } else { // 系統預設測量
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }
複製程式碼

程式碼就是根據當前配置的比例對widthMeasureSpec和heightMeasureSpec重新賦值。

因為我們並沒有改變ImageView的佈局和繪製,所以當重新測量後,仍會按系統預設的方式重新佈局和繪製。

相關文章