# Custom stretchable layout

  • Song strike spring like the sea, thousands of doors night lights such as day

Stretchable layout?

In mobile development, it is common to hide cluttered function buttons with retractable and stretchable containers. Android native requires a ViewGroup to hold all menus. There are often all sorts of interesting and useful requirements in development scenarios that most apis don’t provide, but provide interfaces for placement and sizing to meet the specification of the Android native.

Left extendable layout:Copy the code

Extendable layout on the right:Copy the code

The left side as a whole... Notice that this is not the same as the second oneCopy the code

One, custom content

We can see this in the animation above

  • 1. Click the button to change the background

         The default: Selected:

  • 2. Container layout can be extended in different directions

Two, custom – button

1. The need for

A lot of software on the basic listing surface has a button switching effect as well as we can go to see QQ, wechat, Meituan and so on…

Click on the former After clicking on So in development you’re going to take the selector and you’re going to select the unselected state or you’re going to switch the background in your Activity with a tag and that’s what we did in the early days of development,The coupling degree is very high, the reading ability is very poor, the basic idea of the object oriented, the expansibility is poor, the transplanting reuseIt was all forgotten. Haven’t we written before:

myButton.OnclickListenner{
 if(flag){
   myButton.setBackgroundResource(R.drawable.xxxx1); 
 }else{ myButton.setBackgroundResource(R.drawable.xxxx2); } flag=! flag }Copy the code

Often a project this pile of code all over the sky ah, meet a little need should be able to pile again. Design a delay animation, click process and intermediate transition image…. This code can’t be done in seven or eight lines, maybe 200 lines * countless places = thousands of lines? . Not only does it not conform to the basic principles of writing code, but it also makes us lazy. Code is our lover, and you need to make her beautiful with it.

2. Elegant code

  • We are used to writing views and viewgroups in XML form. Customization brings us great convenience to define beautiful and useful views as before. XML is extremely convenient and separate:

<com.zj.utils.utils.view.LHC_SelectedImageView
  android:gravity="center"
  <!--Set the default background image-->
  app:defaultImag="@drawable/bdmap_close"
  <! -- Set the selected state of the background image -->
  app:selectedImg="@drawable/bdmap_open"
  android:id="@+id/bd_map_setting1"
  android:layout_width="@dimen/dp_35"
  android:layout_height="@dimen/dp_35"/>
Copy the code

3. Customization-background switch

Because it’s a background switch, we usually use ImageView which has a background property, so it’s a lot easier to handle. First let’s write a custom class – code section

  • attributeThe default setting.Select the background.
class LHC_SelectedImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : 
androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyle) {
    var default_img: Drawable? = null
    var seleted_img: Drawable? = null
    init {
        val array: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.LHC_SelectedImageView)
        default_img = array.getDrawable(R.styleable.LHC_SelectedImageView_defaultImag)
        seleted_img = array.getDrawable(R.styleable.LHC_SelectedImageView_selectedImg)
    }
}
Copy the code
  • Attribute definition – unknown baidu, create attrs.xml in value

      
<resources>
  <declare-styleable name="LHC_SelectedImageView">
        <attr name="defaultImag" format="reference" />
        <attr name="cendefault_img" format="reference" />
        <attr name="selectedImg" format="reference" />
        <attr name="animal_duration" format="integer" />
    </declare-styleable>
</resources>
Copy the code
  • Set the default image during initialization. So this is a very simple step and you can see what it’s like to set the default background when you initialize it
  init {
        val array: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.LHC_SelectedImageView)
        default_img = array.getDrawable(R.styleable.LHC_SelectedImageView_defaultImag)
        seleted_img = array.getDrawable(R.styleable.LHC_SelectedImageView_selectedImg)
        setDefaultImage()
    }
    private fun setDefaultImage(a) {
        if(default_img! =null)
        this.background = default_img
    }
Copy the code
  • When we clickCannot affect or block its own click event Onclick event, butI need to switch backgroundsHere we may need to know about the event distribution of View. Bosses skip
View.dispatchevent -> view.setonTouchListener -> view.onTouchEvent OnTouchListener is determined by dispatchTouchEvent. If OnTouchListener is not null and returns true, the event is consumed and onTouchEvent is not executed. Otherwise, execute onTouchEvent. A, set the flag to PREPRESSED and set mHasPerformedLongPress=false; Then issue a 115ms later mPendingCheckForTap; B. If UP is not triggered within 115ms, set the flag to PRESSED, clear the PREPRESSED flag, and send a 500-115ms delay message to detect the long-pressed task; C, LongClickListener will be triggered within 500ms (starting from DOWN) : At this point if LongClickListener is not null, will implement the callback, at the same time if LongClickListener onClick returns true, only the mHasPerformedLongPress is set to true; Otherwise mHasPerformedLongPress remains false; MOVE: the main is to check whether the user delimits the control, if delimit: 115ms, directly remove mPendingCheckForTap; After 115ms, remove the PRESSED from the flag and remove the long-pressed check: removeLongPressCallback(); A. If the UP flag is pressed within 115ms, UnsetPressedState, setPressed(false) is executed; Will forward setPress, which can be overridden in the View with the dispatchSetPressed method to receive; B. If the interval is 115ms-500ms, that is, long press has not occurred, remove the long press detection first and execute onClick callback; C, if it is 500 ms after, so there are two situations: i. set onLongClickListener, and onLongClickListener. The onClick returns true, then click onClick events not trigger; Ii. There is no set onLongClickListener or onLongClickListener. OnClick returns false, click onClick events can trigger; D. Finally execute setPressed to refresh the background and then remove the PRESSED flag;Copy the code

The OnClick event will only be triggered if the View’s onTouchEvent is UP without any interference. So we can go DOWN->UP OnTouchEvent once, that is, one click, when event.action= motionEvent.action_up and do the background switch. The code is as follows:


class LHC_SelectedImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyle) {
    var default_img: Drawable? = null
    var seleted_img: Drawable? = null
    // The tag used to switch images
    var flag = false
    // Record a complete click
    var down = false
    init {
        val array: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.LHC_SelectedImageView)
        default_img = array.getDrawable(R.styleable.LHC_SelectedImageView_defaultImag)
        seleted_img = array.getDrawable(R.styleable.LHC_SelectedImageView_selectedImg)
        setDefaultImage()
    }
    private fun setDefaultImage(a) {
        if(default_img! =null)
        this.background = default_img
    }

    // Click onTouchEvent once to complete the background modification.
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        Log.e("onTouchEvent"."onTouchEvent=" + event.action.toString())
        if (event.action == MotionEvent.ACTION_DOWN) {
            down = true
        }
        if (event.action == MotionEvent.ACTION_UP && down) {
            // Set the image when pressed
            setBackgroundImag()
            down = false
        }
        return super.onTouchEvent(event)

    }

    // Modify the background
    private fun setBackgroundImag(a) {
        if(! flag) {this.background =seleted_img
        } else {
            this.background =default_img } flag = ! flag } }Copy the code

Use:

  <com.zj.utils.utils.view.LHC_SelectedImageView
                    android:gravity="center"
                    app:defaultImag="@drawable/bdmap_close"
                    app:selectedImg="@drawable/bdmap_open"
                    android:id="@+id/bd_map_setting"
                    android:layout_width="@dimen/dp_35"
                    android:layout_height="@dimen/dp_35"
                    android:scaleType="fitXY"
                    tools:ignore="ContentDescription" />
Copy the code

Operation effect:

4. Customization – Excessive switching

The most common click effect

   

Special click transition, in order to be more obvious in the middle of the effect, the animation property time set longer

   

   

In the image above, we can see that there are excessive images or background colors in the middle of the click.

We’ve done thatThe default settingandSet the background with one clickThe switch. So how do we do thatIntermediate transitional background? The previous steps are represented belowWe can continue to look for breakthrough points in the flow chart.By comparing the two processes, we can make it clear that the transition background is set first in TouchEvent UP. thendelaySet and display the final background.

The code is as follows:


@Suppress("UNREACHABLE_CODE")
class LHC_SelectedImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyle) {
    var default_img: Drawable? = null
    var seleted_img: Drawable? = null
    var cendefault_img: Drawable? = null
    var flag = false
    var down = false
    var animal_duration = 0

    init {
        val array: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.LHC_SelectedImageView)
        default_img = array.getDrawable(R.styleable.LHC_SelectedImageView_defaultImag)
        cendefault_img = array.getDrawable(R.styleable.LHC_SelectedImageView_cendefault_img)
        seleted_img = array.getDrawable(R.styleable.LHC_SelectedImageView_selectedImg)
        animal_duration = array.getInt(R.styleable.LHC_SelectedImageView_animal_duration, 300)
        setDefaultImage()
    }

    private fun setDefaultImage(a) {
        this.background = default_img
    }

    // Before the click event is executed - intercepts the click event for background modification.
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        Log.e("onTouchEvent"."onTouchEvent=" + event.action.toString())
        if (event.action == MotionEvent.ACTION_DOWN) {
            down = true
        }
        if (event.action == MotionEvent.ACTION_UP && down) {
            // Press Set transition background
            setBackgroundImag()
            // Set the final loading background
            setPostBackgroundImage()
            down = false
        }
        return super.onTouchEvent(event)

    }

    private fun setPostBackgroundImage(a) {
        postDelayed({
            if(! flag) {this.background = seleted_img
            } else {
                this.background = default_img } flag = ! flag }, animal_duration.toLong()) }// Change the transition background. If no transition background is set, switch with the first one. Switch between two effects skillfully, you can use cendeault_img to control
    private fun setBackgroundImag(a) {
        if (cendefault_img == null)
            return
        this.background = cendefault_img
    }

}

Copy the code

Use: cendefault_img to control the toggle effect. Of course, you can define a property control yourself, not necessary

   <com.zj.utils.utils.view.LHC_SelectedImageView
                    android:gravity="center"
                    app:defaultImag="@drawable/bdmap_close"
                    app:cendefault_img="@drawable/gray_radius"
                    app:selectedImg="@drawable/bdmap_close"
                    app:animal_duration="300"
                    android:id="@+id/bd_map_setting"
                    android:layout_width="@dimen/dp_35"
                    android:layout_height="@dimen/dp_35"
                    android:scaleType="fitXY"
                    tools:ignore="ContentDescription" />

Copy the code

Final running effect

5. Customization-transition animation

Animation is especially important in all kinds of custom interactions, not only to be flashy and flashy, but also to give the user a better interactive experience.

In the previous section we have implemented a simple background switch and transition background. In the same way we just need to set up various animations between the default background and the end background.


@Suppress("UNREACHABLE_CODE")
class LHC_SelectedImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyle) {
    var default_img: Drawable? = null
    var seleted_img: Drawable? = null
    var cendefault_img: Drawable? = null
    var flag = false
    var down = false
    var animal_duration = 0
    var animalType: Int?

    /** * Zoomed animation interpolation */
    var scale_value_start=1f
    var scale_value_center=1f
    var scale_value_end=1f

    /** * Rotation interval value */
    var rotaion_value_star=0f
    var rotaion_value_center=0f
    var rotaion_value_end=0f

    /** * animation definition */
    varvalueAnimator: ValueAnimator? =null

    init {
        val array: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.LHC_SelectedImageView)
        default_img = array.getDrawable(R.styleable.LHC_SelectedImageView_defaultImag)
        cendefault_img = array.getDrawable(R.styleable.LHC_SelectedImageView_cendefault_img)
        seleted_img = array.getDrawable(R.styleable.LHC_SelectedImageView_selectedImg)
        animal_duration = array.getInt(R.styleable.LHC_SelectedImageView_animal_duration, 300)
        animalType = array.getInt(R.styleable.LHC_SelectedImageView_animal_type, 0)
        // Rotate the animation value
        rotaion_value_star  =array.getFloat(R.styleable.LHC_SelectedImageView_animal_rotaion_value_start,0f)
        rotaion_value_center=array.getFloat(R.styleable.LHC_SelectedImageView_animal_rotaion_value_center,0f)
        rotaion_value_end   =array.getFloat(R.styleable.LHC_SelectedImageView_animal_rotaion_value_end,0f)

        / / zoom
        scale_value_start=array.getFloat(R.styleable.LHC_SelectedImageView_animal_scale_value_start,0f)
        scale_value_center=array.getFloat(R.styleable.LHC_SelectedImageView_animal_scale_value_center,0f)
        scale_value_end=array.getFloat(R.styleable.LHC_SelectedImageView_animal_scale_value_end,0f)
        // Set the default background
        setDefaultImage()
    }

    /*** */ / Set the default background */
    private fun setDefaultImage(a) {
        this.background = default_img
    }

    // Before the click event is executed - intercepts the click event for background modification.
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        Log.e("onTouchEvent"."onTouchEvent=" + event.action.toString())
        if (event.action == MotionEvent.ACTION_DOWN) {
            down = true
        }
        if (event.action == MotionEvent.ACTION_UP && down) {
            // Set the image when pressed
            setBackgroundImag()
            // If there is no animation
            if (animalType == 0) {
                setPostBackgroundImage()
            }
            down = false
        }
        return super.onTouchEvent(event)
    }
    // Set the default background of the delayed image
    private fun setPostBackgroundImage(a) {
        postDelayed({
            setEndBackground()
        }, animal_duration.toLong())
    }


    // Modify the background
    private fun setBackgroundImag(a) {
        if (cendefault_img == null)
            return
        if (animalType == 0) {// Indicates no animation
            this.background = cendefault_img
        } else {// Indicates animation
            if (valueAnimator == null && animalType == 2) {
                valueAnimator = ObjectAnimator.ofFloat(scale_value_start,scale_value_center,scale_value_end)
            } else if(valueAnimator == null&&animalType == 1){ valueAnimator = ObjectAnimator.ofFloat(rotaion_value_star,rotaion_value_center,rotaion_value_end) } valueAnimator? .duration = (animal_duration).toLong() valueAnimator? .addUpdateListener { animation ->this.background = cendefault_img
                //1. Rotate animation
                if (animalType == 1) {
                    this.rotation = animation.animatedValue as Float
                } else {
                    //2. Zoom animation
                    this.scaleX = animation.animatedValue as Float
                    this.scaleY = animation.animatedValue as Float}}// Add a listening animationaddAnimalListenner(valueAnimator) valueAnimator? .start() } }private fun addAnimalListenner(valueAnimator: ValueAnimator?). {
        // Listen for the animation to end and set the final backgroundvalueAnimator? .addListener(object : Animator.AnimatorListener {
            override fun onAnimationStart(animation: Animator?).{}override fun onAnimationEnd(animation: Animator?). {
                //3. When the animation ends
                setEndBackground()
            }

            override fun onAnimationCancel(animation: Animator?).{}override fun onAnimationRepeat(animation: Animator?).{}})}private fun setEndBackground(a) {
        if(! flag) {this.background = seleted_img
        } else {
            this.background = default_img } flag = ! flag } }Copy the code

Custom attributes:

    <declare-styleable name="LHC_SelectedImageView">
        <attr name="defaultImag" format="reference" />
        <attr name="cendefault_img" format="reference" />
        <attr name="selectedImg" format="reference" />
        <attr name="animal_duration" format="integer" />// Rotate the threshold between three animations. The beginning, the middle, the end<attr name="animal_rotaion_value_start" format="float"/>
        <attr name="animal_rotaion_value_center" format="float"/>
        <attr name="animal_rotaion_value_end" format="float"/>// Scale the threshold between three animations. The beginning, the middle, the end<attr name="animal_scale_value_start" format="float" />
        <attr name="animal_scale_value_center" format="float" />
        <attr name="animal_scale_value_end" format="float" />

        <attr name="animal_type" format="string">
            <flag name="rotation" value="0x1" />
            <flag name="scale" value="0x2" />
        </attr>
    </declare-styleable>

Copy the code

Use:

   <com.zj.utils.utils.view.LHC_SelectedImageView
                    android:gravity="center"
                    app:defaultImag="@drawable/bdmap_close"
                    app:cendefault_img="@drawable/bdmap_hot_dft"
                    app:selectedImg="@drawable/bdmap_close"
                    app:animal_duration="500"
                    app:animal_type="rotation"
                    app:animal_rotaion_value_start="0"
                    app:animal_rotaion_value_center="360"
                    app:animal_rotaion_value_end="0"
                    android:id="@+id/bd_map_setting"
                    android:layout_width="@dimen/dp_35"
                    android:layout_height="@dimen/dp_35"
                    android:scaleType="fitXY"
                    tools:ignore="ContentDescription" />
  <com.zj.utils.utils.view.LHC_SelectedImageView
                    android:gravity="center"
                    app:defaultImag="@drawable/bdmap_sd_default"
                    app:selectedImg="@drawable/bdmap_sd_click"
                    android:id="@+id/bd_map_nomal"
                    app:cendefault_img="@drawable/bdmap_hot_dft"
                    app:animal_type="scale"
                    app:animal_scale_value_start="0.8"
                    app:animal_scale_value_center="1.2"
                    app:animal_scale_value_end="1"
                    android:layout_width="@dimen/dp_35"
                    android:layout_height="@dimen/dp_35"
                    android:layout_marginLeft="@dimen/dp_2"
                    android:scaleType="fitXY"
                    tools:ignore="ContentDescription" />

Copy the code

Animation is silky on a real machine. B: well… Gift images look stuck because some key frames are removed from recording and compression.

Customization-scalable ViewGrop

As shown below, we need a linealayout-like container to wrap the main button. And click the first button, you can toggle back and forth to close and stretch.

1. Customize fromLeft to rightStretchable LineaLayout

ViewGoup can be better replaced by LineaLayout, which we can clearly know from the flow chartMeasure the width of all children within the ViewGroup, andGets the width of the first childIt’s the most critical part of our process.From the eastern attitude we can see that the width of LineaLayout is exactly the width of the first child View when entering by defaultmeasurementandputNeed to know:

The ViewRootImpl will call performTraversals() and inside it will call performMeasure(), performLayout, performDraw(). PerformMeasure () calls the outermost ViewGroup's measure()-->onMeasure(), which is abstract but provides measureChildren(), MeasureChild () = measureChild() = measureChild() = measureChild() = measureChild() = measureChild() = measureChild() Then call the child View's measure() to the View's OnMeasure ()-->setMeasureDimension(getDefaultSize(),getDefaultSize()),getDefaultSize() returns measureSpec by default, So the custom wrap_content inherited from the View needs to be overridden. 3. PerformLayout () calls the outermost ViewGroup layout(L,t,r,b), where the View uses setFrame() to set the position of the View's four vertices. In onLayout(abstract method) determine the position of the child View, such as LinearLayout will traverse the child View, loop setChildFrame()--> child View.Layout (). PerformDraw () calls the outermost layer ViewGroup draw(): background.draw()(draw the background), onDraw()(draw itself), dispatchDraw()(draw subviews), onDrawScrollBars()(draw decorations). MeasureSpec consists of two specmodes (UNSPECIFIED, EXACTLY(exact value and Match_parent), AT_MOST(Warp_content), and 30 specSizes as an Int SureSpec is determined by the size of the window and its LayoutParams. Other views are determined by the parent's MeasureSpec and the View's LayoutParams. The ViewGroup has getChildMeasureSpec() to get the child's MeasureSpec. There are three ways to get the width and height after measure() : 1. Get it called in Activity#onWindowFocusChange(); 2. Viet.post (Runnable) posts the code to the end of the message queue. 3.ViewTreeObservable.Copy the code

We need to re-measure the width in onMeasure(widthMeasureSpecs: Int, heightMeasureSpecs: Int) so that the width is the width of the first child View when first drawn:


class LHC_ExpandLinearLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : androidx.appcompat.widget.LinearLayoutCompat(context, attrs, defStyle) {
    private lateinit var oneChildView: View
    override fun onMeasure(widthMeasureSpecs: Int, heightMeasureSpecs: Int) {
        oneChildView=getChildAt(0)
        val widthMeasureSpec = MeasureSpec.makeMeasureSpec(oneChildView.measuredWidth, MeasureSpec.EXACTLY)
        super.onMeasure(widthMeasureSpec, heightMeasureSpecs)
    }
}
Copy the code

XML using

<com.zj.utils.utils.view.LHC_ExpandLinearLayout android:gravity="center_vertical" android:layout_alignParentEnd="true" android:layout_alignParentBottom="true" app:gravity="left" android:layout_marginRight="@dimen/dp_17" android:layout_marginBottom="@dimen/dp_50" android:layout_marginTop="@dimen/dp_20" android:id="@+id/map_more_menu" app:duration="300" android:layout_width="wrap_content" android:layout_height="wrap_content"> <com.zj.utils.utils.view.LHC_SelectedImageView android:gravity="center" app:defaultImag="@drawable/bdmap_close" app:cendefault_img="@drawable/bdmap_hot_dft" app:selectedImg="@drawable/bdmap_close" app:animal_duration="500" app:animal_type="rotation" app:animal_rotaion_value_start="0" app:animal_rotaion_value_center="360" app:animal_rotaion_value_end="0" android:id="@+id/bd_map_setting" android:layout_width="@dimen/dp_35" android:layout_height="@dimen/dp_35" android:scaleType="fitXY" tools:ignore="ContentDescription" /> <com.zj.utils.utils.view.LHC_SelectedImageView android:gravity="center" app:defaultImag="@drawable/bdmap_sd_default" app:selectedImg="@drawable/bdmap_sd_click" android:id="@+id/bd_map_nomal" app:cendefault_img="@drawable/bdmap_hot_dft" App: animal_type = "scale" app: animal_scale_value_start = "0.8" app: animal_scale_value_center = "1.2" app:animal_scale_value_end="1" android:layout_width="@dimen/dp_35" android:layout_height="@dimen/dp_35" android:layout_marginLeft="@dimen/dp_2" android:scaleType="fitXY" tools:ignore="ContentDescription" /> <com.zj.utils.utils.view.LHC_SelectedImageView android:gravity="center" app:defaultImag="@drawable/bdmap_hot_dft" app:selectedImg="@drawable/bdmap_hot_click" android:id="@+id/bd_map_hot" android:layout_width="@dimen/dp_35" android:layout_height="@dimen/dp_35" android:layout_marginLeft="@dimen/dp_2" android:scaleType="fitXY" tools:ignore="ContentDescription" /> <com.zj.utils.utils.view.LHC_SelectedImageView android:gravity="center" app:defaultImag="@drawable/bdmap_color_click" app:selectedImg="@drawable/bdmap_color_default" android:id="@+id/bd_map_red" android:layout_width="@dimen/dp_35" android:layout_height="@dimen/dp_35" android:layout_marginLeft="@dimen/dp_2" android:scaleType="fitXY" tools:ignore="ContentDescription" /> </com.zj.utils.utils.view.LHC_ExpandLinearLayout>Copy the code

Effect: The visible initialization width is the width of the first child View

And then we set the property in the first child View ->android:layout_marginLeft="@dimen/dp_15"At this time we go to see the effect:

The layout_marginLeft shifts itself to the right and doesn’t display all of its size. This is clearly due to measurement. We also need to add margin, padding and so on.Gives the view enough scope to displaySo you can get around all the situations and see the first button.The measurement part we need to bring all the levels of the first childViewmarginandpadingAdd them all up. The code is as follows:

class LHC_ExpandLinearLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : androidx.appcompat.widget.LinearLayoutCompat(context, attrs, defStyle) {
    private lateinit var oneChildView: View
    override fun onMeasure(widthMeasureSpecs: Int, heightMeasureSpecs: Int) {
       // Get the first child View
        oneChildView=getChildAt(0)
        // Get margin
        val marginChildView = oneChildView.layoutParams as MarginLayoutParams
        // Get the size of the child View, including margin and pading
        val oneChildViewWidth=oneChildView.measuredWidth+oneChildView.paddingLeft+oneChildView.paddingRight+ marginChildView.leftMargin +marginChildView.rightMargin
        val oneChildViewHeight=oneChildView.measuredHeight+oneChildView.paddingTop+oneChildView.paddingBottom+ marginChildView.topMargin +marginChildView.bottomMargin
        // find the diagonal
        val diagonalLength= sqrt(oneChildViewWidth.toDouble().pow(2) + oneChildViewHeight.toDouble().pow(2.0))
        < span style = "max-width: 100%; clear: both; min-height: 1em;
        val widthMeasureSpec = MeasureSpec.makeMeasureSpec(diagonalLength.toInt(), MeasureSpec.EXACTLY)
        super.onMeasure(widthMeasureSpec, heightMeasureSpecs)
    }
}
Copy the code

Please ignore the background for obvious contrast

At this point does not feel testableThe default measurementIt’s already perfect. Now let’s see, let’s set the first button to oneanimationThe time is 15 seconds,Wide high, Settings are different:Look at the results:

You can obviously see that becauseInconsistent width and heightwhenPart of it will be blocked when there is a rotation animation. As shown in the following figure, “giant” is the first button. When the rotation animation is executed, the largest one swept is the circle, as long as the width and height meet the maximum width and height.The diameter is the giant diagonalNext we calculate the diagonal length.

override fun onMeasure(widthMeasureSpecs: Int, heightMeasureSpecs: Int) {
        // Get the first child View
        oneChildView=getChildAt(0)
        val marginChildView = oneChildView.layoutParams as MarginLayoutParams
        val oneChildViewWidth=oneChildView.measuredWidth+oneChildView.paddingLeft+oneChildView.paddingRight+ marginChildView.leftMargin +marginChildView.rightMargin
        val oneChildViewHeight=oneChildView.measuredHeight+oneChildView.paddingTop+oneChildView.paddingBottom+ marginChildView.topMargin +marginChildView.bottomMargin
        val diagonalLength= sqrt(oneChildViewWidth.toDouble().pow(2) + oneChildViewHeight.toDouble().pow(2.0))
        < span style = "max-width: 100%; clear: both; min-height: 1em;
        val widthMeasureSpec = MeasureSpec.makeMeasureSpec(diagonalLength.toInt(), MeasureSpec.EXACTLY)
        val heightMeasureSpec = MeasureSpec.makeMeasureSpec(diagonalLength.toInt(), MeasureSpec.EXACTLY)
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    }

Copy the code

We can see that the container itself is wide and tall enough to fit itself, butViewNot inThe center coordinates. In addition to measuring and placing custom View, we will proceed nextputThe position of the child View. As follows, our final position calculation can be seen from the figure:

R-l is the fixed width of the ViewGroup, minus the width of the subview and divided by 2 the distance from the subview to the left of the ViewGoup.

  • ChildView left distance from parent ViewGoup Top =(r-L-onechildViewWidth)/2
  • The distance between child View top and parent ViewGoup top =(b-t-onechildViewheight)/2

So in onLayout, we’re going to place the neutron View in the upper left corner of the parent ViewGoup. The code is as follows:

   override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        super.onLayout(changed, l, t, r, b)
        val oneChildViewWidth=oneChildView.measuredWidth
        val oneChildViewHeight=oneChildView.measuredHeight
        val xl=(r-l-oneChildViewWidth)/2
        val yl=(b-t-oneChildViewHeight)/2
        getChildAt(0).layout(xl,yl,xl+oneChildViewWidth,yl+oneChildViewHeight)
    }

Copy the code

Results: We can see that it can be accommodated regardless of width and height

Of course, our stretchable ViewGroup is not arbitrarily set to excessive width and height. Therefore, the calculation and placement of measurements are simple in the code. We have not calculated the expansion yet. The width of the expansion is the width and margin of all sub-layouts, as well as pading and:

override fun onMeasure(widthMeasureSpecs: Int, heightMeasureSpecs: Int) { lt_width=0 oneChildView=getChildAt(0) for (index in 0 until childCount) { val childView:View=getChildAt(index) val marginChildView = childView.layoutParams as MarginLayoutParams lt_width += childView.measuredWidth+getChildAt(index).paddingLeft+childView.paddingRight+ marginChildView.leftMargin + marginChildView. RightMargin} val diagonalLength = SQRT (oneChildView. MeasuredWidth. ToDouble (). The pow + (2.0) OneChildView. MeasuredHeight. ToDouble (). The pow (2.0)) / / determine whether the if (childRota) {heightMeasureSpec = MeasureSpec.makeMeasureSpec(diagonalLength.toInt(), MeasureSpec.EXACTLY)}else{heightMeasureSpec=heightMeasureSpecs} if(! animalStar){ Log.e("LHC_ExpandLinearLayout1", "onMeasure:${oneChildView.measuredWidth}") widthMeasureSpec = MeasureSpec.makeMeasureSpec(oneChildView.measuredWidth, MeasureSpec.EXACTLY) super.onMeasure(widthMeasureSpec, heightMeasureSpec) }else{ Log.e("LHC_ExpandLinearLayout1", "onMeasure:${oneChildView.measuredWidth}") widthMeasureSpec = MeasureSpec.makeMeasureSpec(animalValue, MeasureSpec.EXACTLY) super.onMeasure(widthMeasureSpec, heightMeasureSpec) } }Copy the code

Setting click time to control animation execution and so on is not wordy

/ * * * * ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ * │ ┌ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┐ │ x │ │ Esc │! 1 2 │ │ @ # % 3 $4 │ │ │ 5 6 7 │ │ & * ^ 8 │ │ │ (9) 0 _ + = │ │ - | \ │ ` ~ │ │ * │ ├ ─ ─ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ─ ─ ┤ │ x │ │ Tab │ │ │ │ │ Q W E R T I │ │ │ Y U │ │ │ P O {[│}] │ BS │ │ x │ ├ ─ ─ ─ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ─ ─ ─ ─ ─ ┤ │ x │ │ Ctrl │ │ │ │ │ │ F G D S A H │ │ │ K J L │ :; │ │ "' Enter │ │ X │ ├ ─ ─ ─ ─ ─ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ─ ─ ─ ┬ ─ ─ ─ ┤ │ X │ │ Shift │ │ X │ │ Z C V │ │ │ N M B , │ │ < >. │? / │ Shift │ Fn │ │ x │ └ ─ ─ ─ ─ ─ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ─ ─ ┬ ┴ ─ ─ ─ ┴ ─ ─ ─ ┴ ─ ─ ─ ┴ ─ ─ ─ ┴ ─ ─ ─ ┴ ─ ─ ┬ ┴ ─ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ┘ │ x │ │ Fn Alt │ │ Space Alt │ │ Win │ HHKB │ x │ └ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ┘ │ * └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ Copyright (c) 2015 Bohai Xinneng All Rights Reserved@authorFeiWang * Version: 1.5 * Created on: 1/21/21 * Description: OsmDroid * E-mail: 1276998208@qq.com
 * CSDN:https://blog.csdn.net/m0_37667770/article
 * GitHub:https://github.com/luhenchang
 */
class LHC_ExpandLinearLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : androidx.appcompat.widget.LinearLayoutCompat(context, attrs, defStyle) {
    // Initialize the first time the width is not written to 0 by default

    private var childRota: Boolean=false

    /** * lt_width * total width of the LinearLayout */
    var lt_width=0

    /** * Gravaty is left that firstChild is oneChildView * if gravaty is right that endingChild is oneChildView * if top * if end */
    lateinit var oneChildView:View
    /** * Select state */
    var animalStar=false

    /** * animation definition */
    varscaleX :ValueAnimator? =null

    /** * Animation value and new layout length */
    var animalValue=0
    / * * * * /
    var animalDuration=0

    /** * flag to control the direction of animation execution */
    var selecteFlag=false

    /** * gragvity */
    var gravityOfParent=0x1
    init {
        val array: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.LHC_ExpandLinearLayout)
        gravityOfParent = array.getInt(R.styleable.LHC_ExpandLinearLayout_gravity,0x1)
        childRota=array.getBoolean(R.styleable.LHC_ExpandLinearLayout_child_rotation,false)
        animalDuration=array.getInt(R.styleable.LHC_ExpandLinearLayout_duration,300)
        viewTreeObserver.addOnGlobalLayoutListener {
            setLayout()
        }
    }
    private fun setLayout(a) {

        oneChildView.setOnClickListener {
            if(scaleX==null) { scaleX = ObjectAnimator.ofFloat(oneChildView.measuredWidth.toFloat(), lt_width.toFloat()) } scaleX? .duration = animalDuration.toLong() scaleX? .addUpdateListener { animation -> animalValue = MeasureSpec.makeMeasureSpec((animation.animatedValueas Float).toInt(), MeasureSpec.EXACTLY)
                requestLayout()
            }
            if(! selecteFlag){ scaleX? .start() }else{ scaleX? .reverse() } selecteFlag=! selecteFlag animalStar=true}}private var heightMeasureSpec: Int = 0
    private var widthMeasureSpec:Int=0

    override fun onMeasure(widthMeasureSpecs: Int, heightMeasureSpecs: Int) {
        lt_width=0
        oneChildView=getChildAt(0)
        for (index in 0 until childCount) {
            val childView:View=getChildAt(index)
            val marginChildView = childView.layoutParams as MarginLayoutParams
            lt_width += childView.measuredWidth+getChildAt(index).paddingLeft+childView.paddingRight+ marginChildView.leftMargin +marginChildView.rightMargin
        }
        val diagonalLength= sqrt(oneChildView.measuredWidth.toDouble().pow(2.0) + oneChildView.measuredHeight.toDouble().pow(2.0))
        if (childRota) {
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(diagonalLength.toInt(), MeasureSpec.EXACTLY)
        }else{
            heightMeasureSpec=heightMeasureSpecs
        }
        if(! animalStar){ Log.e("LHC_ExpandLinearLayout1"."onMeasure:${oneChildView.measuredWidth}")
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(oneChildView.measuredWidth, MeasureSpec.EXACTLY)
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        }else{
            Log.e("LHC_ExpandLinearLayout1"."onMeasure:${oneChildView.measuredWidth}")
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(animalValue, MeasureSpec.EXACTLY)
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        }
    }


}

Copy the code

1. Customize fromFrom right to leftStretchable LineaLayout

We’ll see that the parent container of the first child View has plenty of room to rotate around once the measurement is done, but the problem is that the child View is not in the middle. We just gave the child View enough space, but we didn’t put it in the middle of the space, and then we put the child View.Similarly, we find that the second, third, and fourth left are only integer multiples of the first one by R(diagonal). The following code can be obtained:

  for (index in 0 until childCount){
                val childViewWidth=getChildAt(index).measuredWidth
                val childViewHeight=getChildAt(index).measuredHeight
                val diagonalLength1= sqrt(childViewWidth.toDouble().pow(2.0) + childViewHeight.toDouble().pow(2.0))
                getChildAt(index).layout(diagonalLength1.toInt()*index+((diagonalLength1-getChildAt(1).measuredWidth)/2).toInt(),((diagonalLength1-starchild.measuredHeight)/2).toInt(),diagonalLength1.toInt()*index+((diagonalLength1-starchild.measuredWidth)/2).toInt()+starchild.measuredWidth,((diagonalLength1-starchild.measuredHeight)/2).toInt()+starchild.measuredHeight)

            }
Copy the code

Here we can see that no matter how we rotate it, we’re not going to cover any part of the subview.

So here we seem to have a pretty good ending to the rotation problem. What if we put the view on the right? Is that what we want? You can imagine our first child view going to the left when it expands. It’s not where it was. It will run from last to first, as shown below:

And what we want is for the button to always be at the end. So the first View defaults to the position of the last button, whereas when you expand it, you want to put the last button in the first position in exchange. The code is as follows:

package com.zj.utils.utils.view

import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context
import android.content.res.TypedArray
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.widget.GridLayout
import androidx.core.view.doOnLayout
import androidx.core.view.marginLeft
import androidx.core.view.marginRight
import androidx.core.view.marginTop
import com.zj.utils.R
import kotlin.math.pow
import kotlin.math.sqrt

/ * * * * ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ * │ ┌ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┬ ─ ─ ─ ┐ │ x │ │ Esc │! 1 2 │ │ @ # % 3 $4 │ │ │ 5 6 7 │ │ & * ^ 8 │ │ │ (9) 0 _ + = │ │ - | \ │ ` ~ │ │ * │ ├ ─ ─ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ─ ─ ┤ │ x │ │ Tab │ │ │ │ │ Q W E R T I │ │ │ Y U │ │ │ P O {[│}] │ BS │ │ x │ ├ ─ ─ ─ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ─ ─ ─ ─ ─ ┤ │ x │ │ Ctrl │ │ │ │ │ │ F G D S A H │ │ │ K J L │ :; │ │ "' Enter │ │ X │ ├ ─ ─ ─ ─ ─ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ┬ ─ ┴ ─ ─ ─ ─ ┬ ─ ─ ─ ┤ │ X │ │ Shift │ │ X │ │ Z C V │ │ │ N M B , │ │ < >. │? / │ Shift │ Fn │ │ x │ └ ─ ─ ─ ─ ─ ┬ ─ ─ ┴ ┬ ─ ─ ┴ ─ ─ ┬ ┴ ─ ─ ─ ┴ ─ ─ ─ ┴ ─ ─ ─ ┴ ─ ─ ─ ┴ ─ ─ ─ ┴ ─ ─ ┬ ┴ ─ ─ ─ ┴ ┬ ─ ─ ┴ ┬ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ┘ │ x │ │ Fn Alt │ │ Space Alt │ │ Win │ HHKB │ x │ └ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ┘ │ * └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ Copyright (c) 2015 Bohai Xinneng All Rights Reserved@authorFeiWang * Version: 1.5 * Created on: 1/21/21 * Description: OsmDroid * E-mail: 1276998208@qq.com
 * CSDN:https://blog.csdn.net/m0_37667770/article
 * GitHub:https://github.com/luhenchang
 */
class LHC_ExpandLinearLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : androidx.appcompat.widget.LinearLayoutCompat(context, attrs, defStyle) {
    // Initialize the first time the width is not written to 0 by default

    private var childRota: Boolean=false

    /** * lt_width * total width of the LinearLayout */
    var lt_width=0

    /** * Gravaty is left that firstChild is oneChildView * if gravaty is right that endingChild is oneChildView * if top * if end */
    lateinit var oneChildView:View
    var oneChildViewWidth=0
    /** * Select state */
    var animalStar=false

    /** * animation definition */
    varscaleX :ValueAnimator? =null

    /** * Animation value and new layout length */
    var animalValue=0
    / * * * * /
    var animalDuration=0

    /** * flag to control the direction of animation execution */
    var selecteFlag=false

    /** * gragvity */
    var gravityOfParent=0x1
    init {
        val array: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.LHC_ExpandLinearLayout)
        gravityOfParent = array.getInt(R.styleable.LHC_ExpandLinearLayout_gravity,0x1)
        childRota=array.getBoolean(R.styleable.LHC_ExpandLinearLayout_child_rotation,false)
        animalDuration=array.getInt(R.styleable.LHC_ExpandLinearLayout_duration,300)
        viewTreeObserver.addOnGlobalLayoutListener {
            setLayout()
        }
    }
    private fun setLayout(a) {

        oneChildView.setOnClickListener {
            if(scaleX==null) { scaleX = ObjectAnimator.ofFloat(oneChildViewWidth.toFloat(), lt_width.toFloat()) } scaleX? .duration = animalDuration.toLong() scaleX? .addUpdateListener { animation -> animalValue = MeasureSpec.makeMeasureSpec((animation.animatedValueas Float).toInt(), MeasureSpec.EXACTLY)
                requestLayout()
            }
            if(! selecteFlag){ scaleX? .start() }else{ scaleX? .reverse() } selecteFlag=! selecteFlag animalStar=true}}override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        val starchild=getChildAt(0)
        val endchild=getChildAt(childCount-1)
        super.onLayout(changed, l, t, r, b)
        if(selecteFlag&&(gravityOfParent==0x2)) {/ / if
            if(childRota){// Rotated sublayouts need to be reordered
                // Put it first
                for (index in 0 until childCount){
                    val childViewWidth=getChildAt(index).measuredWidth
                    val childViewHeight=getChildAt(index).measuredHeight
                    val diagonalLength= sqrt(childViewWidth.toDouble().pow(2.0) + childViewHeight.toDouble().pow(2.0))
                    getChildAt(index).layout(diagonalLength.toInt()*index+((diagonalLength-getChildAt(1).measuredWidth)/2).toInt(),((diagonalLength-starchild.measuredHeight)/2).toInt(),diagonalLength.toInt()*index+((diagonalLength-starchild.measuredWidth)/2).toInt()+starchild.measuredWidth,((diagonalLength-starchild.measuredHeight)/2).toInt()+starchild.measuredHeight)

                }
                // Switch places
                val starchild_m=getChildAt(0)
                val endchild_m=getChildAt(childCount-1)
                // Attach prevents {@see #onMeasure()} from being modified after execution
                val startLeft=endchild_m.left
                val startTop=endchild_m.top
                val startRight=endchild_m.right
                val startBootom=endchild_m.bottom
                getChildAt(childCount-1).layout(starchild_m.left,starchild_m.top,starchild_m.right,starchild_m.bottom)
                getChildAt(0).layout(startLeft,startTop,startRight,startBootom)
            }else{
                val startRight=starchild.right
                val measuredHeight=endchild.measuredHeight
                starchild.layout(endchild.left, 0,endchild.right, starchild.measuredHeight)
                endchild.layout(0.0,startRight,measuredHeight)
            }

        }else{
            if(childRota)
            for (index in 0 until childCount){
                val childViewWidth=getChildAt(index).measuredWidth
                val childViewHeight=getChildAt(index).measuredHeight
                val diagonalLength1= sqrt(childViewWidth.toDouble().pow(2.0) + childViewHeight.toDouble().pow(2.0))
                getChildAt(index).layout(diagonalLength1.toInt()*index+((diagonalLength1-getChildAt(1).measuredWidth)/2).toInt(),((diagonalLength1-starchild.measuredHeight)/2).toInt(),diagonalLength1.toInt()*index+((diagonalLength1-starchild.measuredWidth)/2).toInt()+starchild.measuredWidth,((diagonalLength1-starchild.measuredHeight)/2).toInt()+starchild.measuredHeight)

            }
        }
    }
    private var heightMeasureSpec: Int = 0
    private var widthMeasureSpec:Int=0

    override fun onMeasure(widthMeasureSpecs: Int, heightMeasureSpecs: Int) {
        lt_width=0
        oneChildView=getChildAt(0)
        for (index in 0 until childCount) {
            if(! childRota) {val childView: View = getChildAt(index)
                val marginChildView = childView.layoutParams as MarginLayoutParams
                lt_width += childView.measuredWidth + childView.paddingLeft + childView.paddingRight + marginChildView.leftMargin + marginChildView.rightMargin
            }else{
                val childView:View=getChildAt(index)
                val childViewWidth=childView.measuredWidth
                val childViewHeight=childView.measuredHeight
                val diagonalLength= sqrt(childViewWidth.toDouble().pow(2.0) + childViewHeight.toDouble().pow(2.0))
                lt_width +=diagonalLength.toInt()
            }
        }

        val childViewWidth=oneChildView.measuredWidth
        val childViewHeight=oneChildView.measuredHeight
        val diagonalLength= sqrt(childViewWidth.toDouble().pow(2.0) + childViewHeight.toDouble().pow(2.0))
        if(childRota){
            oneChildViewWidth=diagonalLength.toInt()

        }else{
            oneChildViewWidth=oneChildView.measuredWidth
        }
        if (childRota) {
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(diagonalLength.toInt(), MeasureSpec.EXACTLY)
        }else{
            // We should simplify to find the highest child of each View
            heightMeasureSpec=MeasureSpec.makeMeasureSpec(childViewHeight, MeasureSpec.AT_MOST)
        }
        if(! animalStar){// If the subview has rotation
            if(childRota){
                widthMeasureSpec = MeasureSpec.makeMeasureSpec(diagonalLength.toInt(), MeasureSpec.EXACTLY)
            }else{
                widthMeasureSpec = MeasureSpec.makeMeasureSpec(oneChildView.measuredWidth, MeasureSpec.EXACTLY)

            }
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        }else{
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(animalValue, MeasureSpec.EXACTLY)
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        }
    }


}


Copy the code

In the XML

 <com.zj.utils.utils.view.LHC_ExpandLinearLayout
            android:layout_alignParentEnd="true"
            android:layout_alignParentBottom="true"
            app:child_rotation="true"
            app:gravity="right"
            android:layout_marginRight="@dimen/dp_13"
            android:layout_marginBottom="@dimen/dp_80"
            android:layout_marginTop="@dimen/dp_20"
            android:id="@+id/map_more_menu1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
            <com.zj.utils.utils.view.LHC_SelectedImageView
                android:gravity="center"
                app:defaultImag="@drawable/bdmap_close"
                app:selectedImg="@drawable/bdmap_open"
                android:id="@+id/bd_map_setting1"
                android:layout_width="@dimen/dp_35"
                android:layout_height="@dimen/dp_35"
                android:layout_marginBottom="@dimen/dp_30"
                android:scaleType="fitXY"
                tools:ignore="ContentDescription" />
            <com.zj.utils.utils.view.LHC_SelectedImageView
                android:gravity="center"
                app:defaultImag="@drawable/bdmap_sd_default"
                app:selectedImg="@drawable/bdmap_sd_click"
                android:id="@+id/bd_map_nomal1"
                android:layout_width="@dimen/dp_35"
                android:layout_height="@dimen/dp_35"
                android:layout_marginLeft="@dimen/dp_2"
                android:scaleType="fitXY"
                tools:ignore="ContentDescription" />
            <com.zj.utils.utils.view.LHC_SelectedImageView
                android:gravity="center"
                app:defaultImag="@drawable/bdmap_hot_dft"
                app:selectedImg="@drawable/bdmap_hot_click"
                android:id="@+id/bd_map_hot1"
                android:layout_width="@dimen/dp_35"
                android:layout_height="@dimen/dp_35"
                android:layout_marginLeft="@dimen/dp_2"
                android:scaleType="fitXY"
                tools:ignore="ContentDescription" />
            <com.zj.utils.utils.view.LHC_SelectedImageView
                android:gravity="center"
                app:defaultImag="@drawable/bdmap_color_click"
                app:selectedImg="@drawable/bdmap_color_default"
                android:id="@+id/bd_map_red1"
                android:layout_width="@dimen/dp_35"
                android:layout_height="@dimen/dp_35"
                android:layout_marginLeft="@dimen/dp_2"
                android:scaleType="fitXY"
                tools:ignore="ContentDescription" />
        </com.zj.utils.utils.view.LHC_ExpandLinearLayout>
Copy the code

The end result: The phone is silky and slippery due to the lag caused by the screen projector.

conclusion

  • Customizing can be done by anyone,measurement+put+drawWith some math and animation, you can make a good interactive View. It requires a lot of hands, persistence and patience.