RatioImageView implements effects such as ImageView scaling

0. Source code address

Github.com/zhxhcoder/X…

1. Reference method

compile 'com. ZHXH: ximageviewlib: 1.2'
Copy the code

2. Usage

Here’s an example:

        <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" />
Copy the code

Riv_height_to_width_ratio =0.48; layout_width=”100dp”; layout_height=”48dp”;

Implementation effect

3. Source code implementation

3.1 Attribute Definition and description

    <declare-styleable name="RatioImageView"> <! -- Width is measured according to the scale of SRC image (height is known) --> <attr name="riv_is_width_fix_drawable_size_ratio" format="boolean"/ > <! Height is measured according to the scale of the SRC image (width is known) --> <attr name="riv_is_height_fix_drawable_size_ratio" format="boolean"/ > <! - when mIsWidthFitDrawableSizeRatio effect, maximum width - > < attr name ="riv_max_width_when_width_fix_drawable" format="dimension"/ > <! - when mIsHeightFitDrawableSizeRatio effect - > < attr name ="riv_max_height_when_height_fix_drawable" format="dimension"/ > <! -- Height setting, reference width, such as 0.5, means height = width ×0.5 --> <attr name="riv_height_to_width_ratio" format="float"/ > <! -- Width setting, reference height, such as 0.5, means width = height ×0.5 --> <attr name="riv_width_to_height_ratio" format="float"/ > <! -- Width and height, to avoid the case where layout_width/layout_height will be treated specially if the screen size is exceeded --> <attr name="riv_width" format="dimension" />
        <attr name="riv_height" format="dimension" />
    </declare-styleable>
Copy the code

3.2 Code Implementation

1. Attribute initialization Initializes associated attributes from 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();
    }
Copy the code

2. Initialize key data

mDrawableSizeRatio = -1f; // SRC image (foreground) width to height ratio

Call the following code in the constructor when mDrawable is not null

            mDrawableSizeRatio = 1f * getDrawable().getIntrinsicWidth()
                    / getDrawable().getIntrinsicHeight();
Copy the code

Other variables are initialized

private boolean mIsWidthFitDrawableSizeRatio; / / whether the width according to the proportion of SRC image (picture) to measure the height (known) private Boolean mIsHeightFitDrawableSizeRatio; / / height is according to the proportion of SRC image (picture) to measure (width is known) private int mMaxWidthWhenWidthFixDrawable = 1; / / when mIsWidthFitDrawableSizeRatio effect, maximum width private int mMaxHeightWhenHeightFixDrawable = 1; / / when mIsHeightFitDrawableSizeRatio effect, maximum height/wide/high privatefloatmWidthRatio = -1; // Width = height *mWidthRatio privatefloatmHeightRatio = -1; // Height = width *mHeightRatio private int mDesiredWidth = -1; Private int mDesiredHeight = -1; private int mDesiredHeight = -1; private int mDesiredHeight = -1;Copy the code

We override the ImageView setImageResource and setImageDrawable functions to customize the generated drawable object

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

    @Override
    public void setImageDrawable(Drawable drawable) {
        super.setImageDrawable(drawable);
        reSetDrawable();
    }
Copy the code

Customize the desired drawable object

    private void reSetDrawable() {
        Drawable drawable = getDrawable();
        if(drawable ! = null) {// Change the layoutif (mIsWidthFitDrawableSizeRatio || mIsHeightFitDrawableSizeRatio) {
                float old = mDrawableSizeRatio;
                mDrawableSizeRatio = 1f * drawable.getIntrinsicWidth()
                        / drawable.getIntrinsicHeight();
                if(old ! = mDrawableSizeRatio && mDrawableSizeRatio > 0) { requestLayout(); }}}}Copy the code

**requestLayout() is called when the image size is different from the defined size. ** rearrange the layout. What does this approach do? We go to the **requestLayout()** method:


    /**
     * 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 logicif 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;
        }
    }
Copy the code

Above is the definition of this method in Android View. From the code, we can see that it first determines whether the current view tree is in the layout process, and then sets the tag bit for the current child view. The function of the tag bit is to mark the current view is to be rearranged. We then call the mparent. requestLayout method, which is important because we are asking the parent for a layout. We call the requestLayout method of the parent and add the PFLAG_FORCE_LAYOUT flag bit to the parent. The parent container in turn calls its parent’s requestLayout method, requestLayout events cascading up to the DecorView, the root View, which in turn passes to the ViewRootImpl, The requestLayout event of the child View will be received and processed by the ViewRootImpl. As you can see, the up-passing process is a chain of responsibility pattern, passing the event up until a superior can handle the event. In this case, only ViewRootImpl can handle requestLayout events.

    @Override
    public void requestLayout() {
        if(! mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested =true; scheduleTraversals(); }}Copy the code

Let’s dig a little deeper and see that in ViewRootImpl, the requestLayout method has been overwritten. In this case, the scheduleTraversals method is called, which is an asynchronous method that eventually calls the ViewRootImpl#performTraversals method, which is the core method of the View workflow. Inside this method, Measure, layout and draw methods are respectively called to carry out the three workflow of View. The three workflow has been described in detail in the previous several articles, and here is a little supplementary explanation. First look at the View#measure method:

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

    if((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || widthMeasureSpec ! = mOldWidthMeasureSpec || heightMeasureSpec ! = mOldHeightMeasureSpec) { ... Omit extraneous code...if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // measure ourselves, this should setthe measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; }... Omit extraneous code... mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; }}Copy the code

The first step is to determine the flag bit. If the flag bit of the current View is PFLAG_FORCE_LAYOUT, the measurement process will be carried out and onMeasure will be called to measure the View. Then finally set the flag bit to PFLAG_LAYOUT_REQUIRED. This flag bit is used in the Layout flow of the View, and if the View has this flag bit set, the layout flow will take place. View#layout:

public void layout(int l, int t, int r, int b) { ... Omit extraneous code... // Determine if the flag bit is PFLAG_LAYOUT_REQUIRED, and if so, layout the Viewif(changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b); // Once the onLayout method is complete, clear the PFLAG_LAYOUT_REQUIRED flag bit 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); }}} mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; }Copy the code

When a child View calls requestLayout, it marks the current View and its parent container, and submits it layer by layer until ViewRootImpl processes the event. ViewRootImpl calls three processes, starting with measure. For each view with a marker bit and its child view will be measured, laid out, drawn.

I’ll also briefly describe the internal logic of the View when invalidate and postInvalidate are called. Directly to the conclusion: When a child View calls the invalidate method, a marker bit is added to the View and the parent container is constantly asked to refresh. The parent container calculates the area it needs to redraw until it is passed to the ViewRootImpl and finally the performTraversals method is triggered. Start the View tree redraw process (only the views that need to be redrawn are drawn).

Back in XimageView, we know that onMeasure and onLayout and onDraw will be called when requestLayout is called. Since the scale changes, we need to remeasure it as follows:

@override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  // mIsWidthFitDrawableSizeRatio mIsHeightFitDrawableSizeRatio // mWidthRatio mHeightRatioif(mDrawableSizeRatio > 0) {// Measure the size of the view according to the ratio of width to height of the foregroundif (mIsWidthFitDrawableSizeRatio) {
                mWidthRatio = mDrawableSizeRatio;
            } else if(mIsHeightFitDrawableSizeRatio) { mHeightRatio = 1 / mDrawableSizeRatio; }}if (mHeightRatio > 0 && mWidthRatio > 0) {
            throw new RuntimeException("Height and width cannot be set in percentage at the same time!!");
        }

        if(mWidthRatio > 0) {// Set width int height = 0;if (mDesiredHeight > 0) {
                height = mDesiredHeight;
            } else {
                height = MeasureSpec.getSize(heightMeasureSpec);
            }
            int width = (int) (height * mWidthRatio);
            if(mIsWidthFitDrawableSizeRatio && mMaxWidthWhenWidthFixDrawable > 0 && width > mMaxWidthWhenWidthFixDrawable) {/ / limit the maximum width  width = mMaxWidthWhenWidthFixDrawable; height = (int) (width / mWidthRatio); } super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); }else if(mHeightRatio > 0) {// The width is known, and the height is specified by the ratio int width = 0;if (mDesiredWidth > 0) {
                width = mDesiredWidth;
            } else {
                width = MeasureSpec.getSize(widthMeasureSpec);
            }
            int height = (int) (width * mHeightRatio);
            if(mIsHeightFitDrawableSizeRatio && mMaxHeightWhenHeightFixDrawable > 0 && height > mMaxHeightWhenHeightFixDrawable) { // Limit maximum altitude height = mMaxHeightWhenHeightFixDrawable; width = (int) (height / mHeightRatio); } super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); }else if(mDesiredWidth > 0 && mDesiredWidth > 0) {// If no other attributes are set, both width and height must be set. int height = mDesiredHeight; super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); }elseSuper.onmeasure (widthMeasureSpec, heightMeasureSpec); }}Copy the code

The code simply reassigns widthMeasureSpec and heightMeasureSpec to the currently configured scale.

Since we have not changed the layout and rendering of the ImageView, when remeasuring it, it will be rearranged and rendered the way the system defaults.