自定义View原理篇(1)-Measure过程

1. 简介

  • View的绘制过程分为三部分:measurelayoutdraw

    measure用来测量View的宽和高。
    layout用来计算View的位置。
    draw用来绘制View。

  • 本章主要对measure过程进行详细的分析。

  • 本文源码基于android 27

2. measure的始点

measure是从ViewRootImplperformTraversals()方法开始的:

2.1 ViewRootImpl的performTraversals

    private void performTraversals() {

        //...

        //获得view宽高的测量规格,mWidth和mHeight表示窗口的宽高,lp.widthhe和lp.height表示DecorView根布局宽和高
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//执行测量

        //...
    }

首先会获取view宽高的测量规格,测量规格在下一节会详细讲述,然后就是调用performMeasure()了:

2.2 ViewRootImpl的performMeasure

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {

        //...

        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

        //...
    }

performMeasure()中就是调用Viewmeasure()方法开始进行测量。

3.MeasureSpec

了解measure的过程时,我们须先了解MeasureSpecMeasureSpec,顾名思义,就是测量规格,其决定了一个View的宽和高。

3.1 MeasureSpec组成

MeasureSpec代表一个32位的int值,前2位代表SpecMode,后30位代表SpecSize。其中:SpecMode代表测量的模式,SpecSize值在某种测量模式下的规格大小。来张图解说明:
MeasureSpec组成图解.png

3.2 SpecMode测量模式

测量模式分为三种UNSPECIFIEDEXACTLYAT_MOST,其具体说明如下表所示:

测量模式 说明 应用场景
UNSPECIFIED (未指定) 父容器没有对当前View有任何限制,当前View可以任意取尺寸 系统内部
EXACTLY(精确) 父容器已经确定当前View的大小,无论View想要多大都会在这范围内 match_parent 和 具体数值
AT_MOST(最多) 当前View不能超过父容器规格大小,具体数值由view去决定 wrap-content

3.3 MeasureSpec源码分析

    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;//模式移位数
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;//模式掩码

        //UNSPECIFIED模式:父容器没有对当前View有任何限制,当前View可以任意取尺寸
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        //EXACTLY模式:父容器已经确定当前View的大小,无论View想要多大都会在这范围内
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        //AT_MOST模式:当前View不能超过父容器规格大小,具体数值由view去决定
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        //根据提供的size和mode得到一个测量规格
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            //sUseBrokenMakeMeasureSpec = targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR1;
            //即targetSdkVersion<=17时,size与mode是直接相加的;>17则进行位运算
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);//位运算
            }
        }

        //根据提供的size和mode得到一个测量规格 
        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

        //获取测量模式
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }

        //获取测量大小
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

    }

可以看到,MeasureSpec类还是挺简单的,MeasureSpec类通过将modesize打包成一个32位int值来减少了对象内存分配,并提供了打包和解包的方法。

3.4 确定MeasureSpec值

measure过程中,系统会将ViewLayoutParams和父容器所施加的规则转换成对应的MeasureSpec,然后在onMeasure()方法中根据这个MeasureSpec来确定View的测量宽高。父容器所施加的规则对于DecorView与普通View是不同的,我们分开来看。

3.4.1 确定DecorView的MeasureSpec值

DecorView,作为顶级的View,我们平时setContentView()所设置的布局可能只是DecorView其中的一部分,如下图所示:
DecorView、ContentView、标题栏图解.png
关于DecorView,可以看看我的另一篇文章:从setContentView揭开DecorView

3.4.1.1 ViewRootImpl的PerformTraveals

ViewRootImplPerformTraveals()方法中会获得DecorViewMeasureSpec值:

    private void performTraversals() {

        //...

        //获得view宽高的测量规格,mWidth和mHeight表示窗口的宽高,lp.widthhe和lp.height表示DecorView根布局宽和高
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//执行测量

        //...
    }

3.4.1.2 ViewRootImpl的getRootMeasureSpec

我们再来看看getRootMeasureSpec()方法:

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            //如果View的布局参数为MATCH_PARENT,则其为精确模式,大小为窗口的大小
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            //如果View的布局参数为WRAP_CONTENT,则其为最多模式,大小不定,但不能超过窗口的大小
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            //如果View的布局参数为准确的数值,如100dp等,则其为精确模式,大小为View设定的数值
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

可以看到:DecorView的MeasureSpec值是由窗口大小及其布局参数来决定的。

来张表格总结:

布局参数 LayoutParams.MATCH_PARENT LayoutParams.WRAP_CONTENT 准确数值
MeasureSpec值 EXACTLY
windowSize(窗口大小)
AT_MOST
windowSize(不能超过窗口大小)
EXACTLY
rootDimension(准确数值)

3.4.2 确定普通View的MeasureSpec值

普通ViewMeasureSpec值通过getChildMeasureSpec()方法来获取,getChildMeasureSpec()位于ViewGroup中:

3.4.2.1 ViewGroup的getChildMeasureSpec


    /**
     *
     * @param spec 父容器的测量规格(MeasureSpec)
     * @param padding 子view的内边距和外边距(padding,margin) 
     * @param childDimension 子view的布局参数(宽/高)
     * @return 子view的测量规格(MeasureSpec)
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //父容器的测量模式
        int specMode = MeasureSpec.getMode(spec);
        //父容器的测量大小
        int specSize = MeasureSpec.getSize(spec);

        //子view大小 = 父容器大小-子view边距(子view只在某些地方地方用到这个值,具体看下面的代码)
        int size = Math.max(0, specSize - padding);

        //初始化子view的大小和模式(最终的结果需要下面代码计算出来) 
        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {

        case MeasureSpec.EXACTLY://当父容器模式为EXACTLY时
            if (childDimension >= 0) {// 当子view的LayoutParams>0,即有确切的值
                //子view大小为子自身所赋的值,模式大小为EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {// 当子view的LayoutParams为MATCH_PARENT时(-1)
                //子view大小为父容器剩余大小,模式为EXACTLY
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {// 当子view的LayoutParams为WRAP_CONTENT时(-2)
                //子view决定自己的大小,但最大不能超过父容器剩余大小,模式为AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        case MeasureSpec.AT_MOST: // 当父容器的模式为AT_MOST时
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;


        case MeasureSpec.UNSPECIFIED:// 当父容器的模式为UNSPECIFIED时
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //这里的sUseZeroUnspecifiedMeasureSpec值为targetSdkVersion < Build.VERSION_CODES.M;即<23为true,否则为false.
                //大于等于23时会将size作为提示值,否则为0
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }

        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

可以看到:普通View的MeasureSpec值是由父容器的MeasureSpec及其布局参数来决定的。

来张表格总结:

父容器测量模式(右侧)
———————————-
子View布局参数(下侧)
EXACTLY(精确) AT_MOST(最多) UNSPECIFIED(未指定)
具体dp/px EXACTLY
childDimension(子View尺寸)
EXACTLY
childDimension(子View尺寸)
EXACTLY
childDimension(子View尺寸)
MATCH_PARENT EXACTLY
parentSize(父容器剩余大小)
AT_MOST
parentSize(不能超过父容器剩余大小)
UNSPECIFIED
targetSdkVersion<23 ? 0 : size;
(大于等于23时会将size作为提示值)
WRAP_CONTENT AT_MOST
parentSize(不能超过父容器剩余大小)
AT_MOST
parentSize(不能超过父容器剩余大小))
UNSPECIFIED
targetSdkVersion<23 ? 0 : size;
(大于等于23时会将size作为提示值)

对上表作一些规律总结:

  • 以子View布局参数为标准,横向观察:
  1. 当子View使用具体的dp/px时,无论父容器的测量模式是什么,子View的测量模式都是EXACTLY且大小等于设置的具体数值;
  2. 当子View使用match_parent时,子View的测量模式与父容器的测量模式保存一致。

另外,UNSPECIFIED模式一般很少用到,可以不用过多关注。

4. measure过程分析

measure过程根据View的类型可以分为两种情况:
1. 测量单一View时,只需测量自身;
2. 测量ViewGroup时,需要对ViewGroup中包含的所有子View都进行测量。

我们对这两种情况分别进行分析。

4.1 单一View的measure过程

单一Viewmeasure过程由Viewmeasure()来实现:

4.1.1 View的measure

    /**
     *
     * @param widthMeasureSpec view的宽测量规格
     * @param heightMeasureSpec view的高测量规格
     */
   public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        //...
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        //...
   }

这里传入的widthMeasureSpec/heightMeasureSpec是当前View的测量规格,通过getChildMeasureSpec()来获得的。具体细节可以看上面的分析。
measure()方法为final类型,子类不能对其进行重写measure()方法中主要就是对基本测量逻辑作判断,最终会调用onMeasure()方法。

4.1.2 View的onMeasure

   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

上面就一行代码,看起来很简单,但是包含了三个方法:getSuggestedMinimumWidth()/getSuggestedMinimumHeight()getDefaultSize()setMeasuredDimension(),我们分开来看:

4.1.3 View的getSuggestedMinimumWidth

getSuggestedMinimumHeight()getSuggestedMinimumWidth()同理,我们这里只看下getSuggestedMinimumWidth()

    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

如果View没有设置背景,那么View的宽度为mMinWidth,而mMinWidth就是android:minWidth属性所指定的值,若android:minWidth没有指定,则默认为0;
如果View设置了背景,那么View的宽度为mMinWidthmBackground.getMinimumWidth()中的最大值。

那么mBackground.getMinimumWidth()的值是多少呢,我们来看看:

4.1.4 Drawable的getMinimumWidth

    public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();//返回背景图Drawable的原始宽度
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }

可以看出:mBackground.getMinimumWidth()返回的是背景图Drawable的原始宽度,若无原始宽度则返回0。

那么Drawable什么情况下有原始宽度?如:ShapeDrawable没有,但BitmapDrawable有。

4.1.5 View的getDefaultSize

我们再来看看ViewgetDefaultSize()方法:

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);//获取测量模式
        int specSize = MeasureSpec.getSize(measureSpec);//获取测量大小

        switch (specMode) {
        //测量模式为UNSPECIFIED时,View的测量大小为默认大小,即getSuggestedMinimumWidth()/getSuggestedMinimumHeight()返回的结果。
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        //测量模式为AT_MOST、EXACTLY时,View的测量大小为测量规格中的测量大小
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

4.1.6 View的setMeasuredDimension

再来看setMeasuredDimension()

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        //...
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

setMeasuredDimension()里会调用setMeasuredDimensionRaw()

4.1.7 View的setMeasuredDimensionRaw

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

setMeasuredDimensionRaw()中就是对测量出的宽高进行保存。
到这里,单一Viewmeasure过程就完成了。

4.1.8 时序图

最后,来张相关的时序图:
单一View测量过程时序图.png

4.1.9 流程图

以及来张测量过程细节流程图:
单一View测量过程流程图.png

4.2 ViewGroup的measure过程

由于ViewGroup包含了子View,因此其measure过程是通过遍历所有的子View并对子View测量,然后合并所有子View的尺寸,最后得到ViewGroup的测量值。

ViewGroup测量顺序图.png

如上图所示,要执行ViewGroupmeasure,首先从顶部的ViewGroup开始遍历,自上而下递归执行子Viewmeasure

ViewGroupmeasure过程是从measureChildren()开始的。

4.2.1 ViewGroup的measureChildren

    /**
     *
     * @param widthMeasureSpec ViewGroup的宽测量规格
     * @param heightMeasureSpec ViewGroup的高测量规格
     */
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {//遍历子View
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

再来看measureChild()

4.2.2 ViewGroup的measureChild

    protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();//获得子view的布局参数

        //获得子view的宽/高测量规格
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        //子view开始测量
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

getChildMeasureSpec()的代码请看上面的分析,点击跳上去

4.2.3 View的measure

   public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        //...
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        //...
   }

这里的measure跟上面单一Viewmeasure方法是一样的。

4.2.4 ViewGroup的onMeasure

如果子View是一个单一的View的话,则直接调用ViewonMeasure()方法,跟上面单一Viewmeasure过程是一样大的;
如果子ViewViewGroup的话,理论上应该是应该调用ViewGrouponMeasure()方法的。
但实际上,ViewGroup中是没有onMeasure()这个方法的,ViewGroup作为一个抽象类,需要其子类去实现测量过程的onMeasure()方法,比如LinearLayoutRelativeLayout等。因为LinearLayoutRelativeLayout这些布局具体不同的特性,其测量细节各有不同,无法进行统一的实现,因此需要其子类去进行具体的实现。因此我们自定义ViewGroup时需要重写onMeasure()方法。
我们这里看下LinearLayoutonMeasure()实现:

4.2.5 LinearLayout的onMeasure

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {//方向判断
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

LinearLayout会区分方向来进行不同的测量方法,我们主要看下竖向的测量measureVertical(),横向的原理差不多这里就不看了。

4.2.6 LinearLayout的measureVertical

    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {

        //获取子view数量
        final int count = getVirtualChildCount();

        //获取LinearLayout的宽/高测量模式
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //...

        for (int i = 0; i < count; ++i) {//遍历子View
            final View child = getVirtualChildAt(i);

            //..

            // 最终会调用子View的measure(),计算出子View的大小
            measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

            // 获取子View的测量高度
            final int childHeight = child.getMeasuredHeight();

            //..

            final int totalLength = mTotalLength;

            // 将子View的测量高度以及Margin加到LinearLayout的总高度上
            mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));
        }

        //..

        // 加上LinearLayout设置的Padding
        mTotalLength += mPaddingTop + mPaddingBottom;

        int heightSize = mTotalLength;
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);

        //..

        //保存测量值
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),heightSizeAndState);


    }

最后会调用setMeasuredDimension()来保存测量好的值,至此ViewGroup的测量过程就完成了。

4.2.7 时序图

最后,来张相关的时序图:

ViewGroup测量过程时序图.png

4.2.8 流程图

以及来张ViewGroup测量过程细节流程图:
ViewGroup测量过程流程图.png

5. 自定义View

5.1 自定义单一View

自定义单一View,可以直接使用View中默认定义的onMeasure()方法。如果有时需要更精准的测量,可以重写onMeasure()

5.2 自定义ViewGroup

由于ViewGroup没实现onMeasure(),所以自定义ViewGroup需要重写onMeasure()方法。这里给个简单的模板:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //定义存放测量后的View的宽和高的变量
        int widthMeasure;
        int heightMeasure;


        //实现测量方法以及具体的测量细节
        measureMethod();

        //必不可少  保存测量后View宽和高的值
        setMeasuredDimension(widthMeasure, heightMeasure);
    }

6. 其他问题

6.1 获取View的测量宽高

我们可以使用getMeasuredWidth()getMeasuredHeight()来获取View测量出的宽高。

但是从上面的代码分析可以看到,在调用setMeasuredDimension()方法之后,我们才能使用getMeasuredWidth()getMeasuredHeight()来获取View测量出的宽高,在这之前去调用这两个方法得到的值都会是0。

结合Activity的启动过程以及生命周期,在onCreate()onResume()中,都还未开始进行测量的操作,所以这时候调用getMeasuredWidth()getMeasuredHeight()的值都会是0。开始测量的相关代码最早可以追涉到ActivityThreadhandleResumeActivity()方法,可以看下这篇文章的介绍:Activity启动过程详解
另外,在某些情况下,需要多次测量才能确定View最终宽高;因此这种情况下获得的值可能是不准确的,建议在layout过程中onLayout()去获取最终宽高。

6.2 自定义View时WRAP_CONTENT不生效的问题

从上面getDefaultSize()方法的代码可以看到,无论是AT_MOST模式还是EXACTLY模式,View的测量大小都为测量规格中的测量大小,所以我们就会看到自定义View使用WRAP_CONTENT会起到跟MATCH_PARENT的效果。

为了解决这个问题需要在LayoutParams属性为WRAP_CONTENT时指定一下默认的宽和高,如:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width=100;//设置一个默认宽
        int height=100;//设置一个默认高

        if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
            setMeasuredDimension(width, height);//宽高都为WRAP_CONTENT都设置为默认宽高
        } else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            setMeasuredDimension(width, heightSize);//只有宽为WRAP_CONTENT时,只设置默认宽
        } else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
            setMeasuredDimension(widthSize, height);//只有高为WRAP_CONTENT时,只设置默认高
        }
    }

这里的默认值请根据实际情况去确认。
像系统的TextView这些都支持WRAP_CONTENT,可以去看下其onMeasure()的源码实现。
如果我们自定义的View只需要设置指定的具体宽高或者MATCH_PARENT,可以不用重写onMeasure()方法。

猜你喜欢

转载自blog.csdn.net/u011810352/article/details/79380439