Android自定义一个可伸展的ViewGroup

e16b90c29e4463e142006d67b8735370.jpeg

/   今日科技快讯   /

近日多家媒体报道,有认证为阿里巴巴集团的员工在职场社交平台称,“88VIP积分将可以免费兑换腾讯视频会员,已经在内部灰度测试,预计双十一前上线”。但是腾讯视频方面暂时否认了这一消息。

/   作者简介   /

今年最长的工作日周终于要结束了,大家好好休息吧,我们下周再见!

本篇文章转自路很长OoO的博客,文章主要分享了可伸展/收缩的ViewGroup,相信会对大家有所帮助!

原文地址:

https://juejin.cn/post/6920498961056956430

/   可伸展布局   /

移动端开发,对于杂乱的功能按钮用可收起和伸展容器隐藏起来往往很常见。android原生需要一个ViewGroup可容纳所有菜单。往往在开发场景中都会有着各种好玩又有用的需求,大多数API没法提供,但提供了接口来摆放位置和测量大小满足android原生端的制定性。

左侧可伸展布局:

53179f03cd392fb3d5e0b4be203cbd70.gif

右侧可伸展布局:

c982995ee16482416902c11eece3b1dd.gif

左侧整体展开...注意了这里不一样和第二种

12917d0bbea39c8c402cfc117528937b.gif

一、自定义内容

上图动画我们可以看到

1. 可以点击切换背景的按钮 默认:  选中:

2. 可以伸展不同方向的容器布局

二、自定义按钮

  • 必要性

基本上市面上很多的软件都有着按钮切换效果不妨我们可以去看看qq、微信、美团等...

4b8159a7361321249eca9f4e6f5b02f9.jpeg

771b1d4112bddd09ecb43bdf652cc237.gif

开发中拿着selector设置选中未选中状态或者在Activity中通过标记进行切换背景是我们开发初期长干的事,耦合度极高,阅读性极差,面向对象的基本思想,扩展性差,移植复用性都被抛之脑后呀。我们以前是不是写过:

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

往往一个项目这一堆代码漫天跑呀,遇到一点儿需求应该可以再堆一堆。设计个延迟动画,点击过程加个中间过度图片等....这些代码就不是七八行能搞定的,也许两百行*无数个地方使用=几千行?....不仅不符合写代码的基本原则,也显得我们太懒惰,代码是我们情人,你用它就要让她漂亮起来。

  • 优雅代码

我们常常熟练的写着xml形式的View和ViewGroup,自定义为我们带来着极大地方便能让我们像以前一样定义及其漂亮好用的View,xml的使用格外方便又分离:

<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"/>
  • 自定义-背景切换

fcaf6db4e92e0dadf7b65b42fc0399d1.gif

由于是背景的切换一般我们使用ImageView本身就有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)
    }
}

属性定义--不清楚的百度,在value中新建attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<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>

初始化时候设置默认图片。这一步很简单都能看懂吧初始化时候设置默认的背景

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() {
        if (default_img!=null)
        this.background = default_img
    }

点击时候我们不能影响和阻断自身的点击事件Onclick事件,但需要切换背景,这里可能需要我们对View的事件分发需要了解一下。大佬略过

View事件分发如果看过源码或者看过别人博客,都有点记忆吧应该,大概列举在下面

(1)、View.dispatchEvent->View.setOnTouchListener->View.onTouchEvent 在dispatchTouchEvent中会进行OnTouchListener的判断,如果OnTouchListener不为null且返回true,则表示事件被消费,onTouchEvent不会被执行;否则执行onTouchEvent。

(2)、onTouchEvent中的DOWN,MOVE,UP

DOWN时:

a、首先设置标志为PREPRESSED,设置mHasPerformedLongPress=false ;然后发出一个115ms后的mPendingCheckForTap;

b、如果115ms内没有触发UP,则将标志置为PRESSED,清除PREPRESSED标志,同时发出一个延时为500-115ms的,检测长按任务消息;

c、如果500ms内(从DOWN触发开始算),则会触发LongClickListener: 此时如果LongClickListener不为null,则会执行回调,同时如果LongClickListener.onClick返回true,才把mHasPerformedLongPress设置为true;否则mHasPerformedLongPress依然为false;

MOVE时:

主要就是检测用户是否划出控件,如果划出了:

115ms内,直接移除mPendingCheckForTap;

115ms后,则将标志中的PRESSED去除,同时移除长按的检查:removeLongPressCallback();

UP时:

a、如果115ms内,触发UP,此时标志为PREPRESSED,则执行UnsetPressedState,setPressed(false);会把setPress转发下去,可以在View中复写dispatchSetPressed方法接收;

b、如果是115ms-500ms间,即长按还未发生,则首先移除长按检测,执行onClick回调;

c、如果是500ms以后,那么有两种情况:

i.设置了onLongClickListener,且onLongClickListener.onClick返回true,则点击事件OnClick事件无法触发;

ii.没有设置onLongClickListener或者onLongClickListener.onClick返回false,则点击事件OnClick事件依然可以触发;

d、最后执行setPressed刷新背景,然后将PRESSED标识去除;

看完上面我们明白OnClick在View的onTouchEvent的UP中没有受到任何干扰时才会通知触发OnClick事件。所以我们可以在OnTouchEvent中一次DOWN->UP即一次点击,当event.action=MotionEvent.ACTION_UP这里进行背景的切换。代码如下:

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 flag = false
    //记录一次完整的点击
    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() {
        if (default_img!=null)
        this.background = default_img
    }

    //在onTouchEvent中点击一次完成进行背景的修改。
    @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) {
            //按下的时候设置图片
            setBackgroundImag()
            down = false
        }
        return super.onTouchEvent(event)

    }

    //修改背景
    private fun setBackgroundImag() {
        if (!flag) {
            this.background =seleted_img
        } else {
            this.background =default_img
        }
        flag = !flag
    }

}

使用:

<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" />

运行效果:

8c0eaf828a76c52a6fea8e07553f4cf3.gif

  • 自定义-过度切换

最常见的点击效果

2a51b6f8a1115242c7f57622c22c1ea9.gif

特别的点击过渡,为了中间的效果更明显,动画属性时间设置的比较长

df34f99f987324e1b4e567ec6625d847.gif

dcfae1b4fd9412c011531e31116d7d2b.gif

上图我们可发现点击中间有过度的图片或背景颜色等。

我们已经实现了默认背景和一次点击完成设置背景的切换。那么我们如何实现中间过渡背景呢?之前的步骤用下面表示

56e4f40da3c3bbb7be3221b2f85af6e1.jpeg

在流程图中我们可以继续往下去找突破点。

eb21d73acd4fdab586fe9f1e7510deea.jpeg

通过两个流程对比,我们可以很明确在TouchEvent UP中首先设置过渡背景。然后延迟设置、显示最终背景。

1fb83f31fb94ddaec614574fbe2dd7ac.jpeg

代码如下:

@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() {
        this.background = default_img
    }

    //在自己点击事件执行之前-》拦截点击事件来进行背景的修改。
    @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) {
            //按下设置过渡背景
            setBackgroundImag()
            //设置最终加载背景
            setPostBackgroundImage()
            down = false
        }
        return super.onTouchEvent(event)

    }

    private fun setPostBackgroundImage() {
        postDelayed({
            if (!flag) {
                this.background = seleted_img
            } else {
                this.background = default_img
            }
            flag = !flag
        }, animal_duration.toLong())
    }

    //修改过渡背景,如果没有设置过渡背景就和第一种切换方式。巧妙完成两种效果切换方式,可以用cendeault_img来控制
    private fun setBackgroundImag() {
        if (cendefault_img == null)
            return
        this.background = cendefault_img
    }

}

使用:cendefault_img有无来控制切换效果,当然你可以自己去定义一个属性控制,显的没必要

<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" />

最终运行效果

9a07643adfc99cc350e64348314923ab.gif

  • 自定义-过度动画

动画往往在各种自定义交互中显的格外的重要,不仅仅是显的高大上且花里胡哨、更能够带给用户更好的交互体验。

0f84b5cfbb83a4a8b353404c6dda0dbe.gif

在上部分我们已经实现了简单的背景切换和过渡背景,同样的思路我们只需要在默认背景和结束背景中间设置各种我们需要的动画即可。

4d591d88f0d1a25687795fbadf18be2b.jpeg

@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?

    /**
     * 缩放的动画插值
     */
    var scale_value_start=1f
    var scale_value_center=1f
    var scale_value_end=1f

    /**
     * 旋转的动画间隔值
     */
    var rotaion_value_star=0f
    var rotaion_value_center=0f
    var rotaion_value_end=0f

    /**
     * 动画定义
     */
    var valueAnimator: 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)
        //旋转动画值
        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)

        //缩放
        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)
        //设置默认背景
        setDefaultImage()
    }

    /***
     * //设置默认背景
     */
    private fun setDefaultImage() {
        this.background = default_img
    }

    //在自己点击事件执行之前-》拦截点击事件来进行背景的修改。
    @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) {
            //按下的时候设置图片
            setBackgroundImag()
            //如果没有动画
            if (animalType == 0) {
                setPostBackgroundImage()
            }
            down = false
        }
        return super.onTouchEvent(event)
    }
    //设置延迟图片默认背景
    private fun setPostBackgroundImage() {
        postDelayed({
            setEndBackground()
        }, animal_duration.toLong())
    }


    //修改背景
    private fun setBackgroundImag() {
        if (cendefault_img == null)
            return
        if (animalType == 0) {//表示没动画
            this.background = cendefault_img
        } else {//表示有动画
            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.旋转动画
                if (animalType == 1) {
                    this.rotation = animation.animatedValue as Float
                } else {
                    //2.缩放动画
                    this.scaleX = animation.animatedValue as Float
                    this.scaleY = animation.animatedValue as Float
                }
            }
            //添加监听动画
            addAnimalListenner(valueAnimator)
            valueAnimator?.start()
        }
    }

    private fun addAnimalListenner(valueAnimator: ValueAnimator?) {
        //监听动画结束且设置最后的背景
        valueAnimator?.addListener(object : Animator.AnimatorListener {
            override fun onAnimationStart(animation: Animator?) {

            }

            override fun onAnimationEnd(animation: Animator?) {
                //3.动画结束时候
                setEndBackground()
            }

            override fun onAnimationCancel(animation: Animator?) {
            }

            override fun onAnimationRepeat(animation: Animator?) {
            }
        })
    }

    private fun setEndBackground() {
        if (!flag) {
            this.background = seleted_img
        } else {
            this.background = default_img
        }
        flag = !flag
    }

}

自定义属性:

<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" />
        //旋转三个动画间阀值。开始、中间、结束
        <attr name="animal_rotaion_value_start" format="float"/>
        <attr name="animal_rotaion_value_center" format="float"/>
        <attr name="animal_rotaion_value_end" format="float"/>
        //缩放三个动画间阀值。开始、中间、结束
        <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>

使用:

<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" />

动画在真机上是很丝滑的。嗯...gift图片因为录屏和压缩去掉了一些关键帧导致看起来有卡顿。

e359a986ee683fb6e41a4fe30ca421d8.gif

三、自定义-可伸缩的ViewGrop

如下图、我们需要一个LineaLayout一样的容器包裹主按钮。且点击第一个按钮,可来回切换关闭和伸展。

bb56fa2869e204537d9dd2ed922e95e2.gif

自定义-从左到右可伸展的LineaLayout

ViewGoup我们可以通过LineaLayout更好的代替,从流程图我们可以明确的知道测量ViewGroup内部所有孩子的宽度 和 获取第一个孩子的宽度是我们流程中最关键的部分。

0f6b23722e559a5b31e44cdfb51a457b.jpeg

通过东态度我们可以发现默认进入时候LineaLayout的宽度正好是第一个子View的宽度,这里我们需要对View的测量和摆放需要去了解一下:

ViewRootImpl 会调用 performTraversals(), 其内部会调用performMeasure()、performLayout、performDraw()。

performMeasure() 会调用最外层的 ViewGroup的measure()-->onMeasure(), ViewGroup 的 onMeasure() 是抽象方法,但其提供了measureChildren(),这之中会遍历子View然后循环调用 measureChild() 这之中会用 getChildMeasureSpec()+父View的MeasureSpec+子View的 LayoutParam一起获取本View的MeasureSpec,然后调用子View的measure()到View的 onMeasure()-->setMeasureDimension(getDefaultSize(),getDefaultSize()),getDefaultSize()默认返回measureSpec的测量数值,所以继承View进行自定义的wrap_content需要重写。 

performLayout()会调用最外层的ViewGroup的layout(l,t,r,b),本View在其中使用setFrame()设置本View的四个顶点位置。在onLayout(抽象方法)中确定子View的位置,如LinearLayout会遍历子View,循环调用setChildFrame()-->子View.layout()。

performDraw() 会调用最外层 ViewGroup的draw():其中会先后调用background.draw()(绘制背景)、onDraw()(绘制自己)、dispatchDraw()(绘制子View)、onDrawScrollBars()(绘制装饰)。

MeasureSpec由2位SpecMode(UNSPECIFIED、EXACTLY(对应精确值和match_parent)、AT_MOST(对应warp_content))和30位SpecSize组成一个int,DecorView的MeasureSpec由窗口大小和其LayoutParams决定,其他View由父View的MeasureSpec和本View的LayoutParams决定。ViewGroup中有getChildMeasureSpec()来获取子View的MeasureSpec。 

三种方式获取measure()后的宽高:

  • Activity#onWindowFocusChange()中调用获取 

  • view.post(Runnable)将获取的代码投递到消息队列的尾部。

  • ViewTreeObservable.

我们需要在onMeasure(widthMeasureSpecs: Int, heightMeasureSpecs: Int)里面进行重新测量宽度让首次绘制时候宽度为第一个子View的宽度:

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)
    }
}

xml使用

<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>

效果:可见初始化宽度为第一个子View的宽度

5983f48aee4d6708aa620315f199289b.gif

然后我们在第一个子View中设置了属性-> android:layout_marginLeft="@dimen/dp_15" 这时候我们去看看效果:

ab6fc74ccc3acb9efb1d623bd1e9e1e2.png

如下我们发现按钮看不全,因为layout_marginLeft让自身像右边平移,而在自身大小的范围内没法全部展示,很明确是测量导致的,我们还得加上margin和padding等部分。给足了视图显示范围才可以规避各种情况看到第一个按钮。

243bd680cf399682d0ab983f68c9ad8c.png

测量部分我们需要将第一个childView所有的水平margin和pading都加起来。代码如下:

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) {
       //获取第一个子View
        oneChildView=getChildAt(0)
        //用来获取margin
        val marginChildView = oneChildView.layoutParams as MarginLayoutParams
        //用来获取子View的视图大小包括margin和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
        //求对角线
        val diagonalLength= sqrt(oneChildViewWidth.toDouble().pow(2) + oneChildViewHeight.toDouble().pow(2.0))
        //精确测量第一个子View宽度,这里我们因为是明确第一个View的宽度所以是精确测量。
        val widthMeasureSpec = MeasureSpec.makeMeasureSpec(diagonalLength.toInt(), MeasureSpec.EXACTLY)
        super.onMeasure(widthMeasureSpec, heightMeasureSpecs)
    }
}

背景请忽视为了对比明显

f348843a14823c11c3d1371bb13fd2a7.png

到此时是不是觉得测试化默认测量已经很完美了。接下来再看我们将第一个按钮设置一个动画时间15秒,宽高、设置不一样:

1ba236e575b01f8c5b3f9297e7e7f734.png

看看效果:

6ff15e708d92b18f98197e888c8cb877.gif

可以明显看出由于宽高不一致当有旋转动画时部分会被遮挡住。那宽度到底是多少才合适如下图:巨型为第一个按钮当旋转动画执行过程中扫过最大的就是圆圈,只要宽和高满足最大宽高即可。直径即巨型的对角线,接下来我们计算对角线长度。

a10e36f8769b131bf37906631829f4a6.jpeg

override fun onMeasure(widthMeasureSpecs: Int, heightMeasureSpecs: Int) {
        //获取第一个子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))
        //精确测量第一个子View宽度,这里我们因为是明确第一个View的宽度所以是精确测量。
        val widthMeasureSpec = MeasureSpec.makeMeasureSpec(diagonalLength.toInt(), MeasureSpec.EXACTLY)
        val heightMeasureSpec = MeasureSpec.makeMeasureSpec(diagonalLength.toInt(), MeasureSpec.EXACTLY)
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    }

518c9a0e6a0b7f5223a576f26467d9f0.png

器自身宽高完全可以放的下自身,但是View不在中心坐标。自定义View除测量还有摆放,接下来我们进行摆放子View的位置。如下,我们最终的位置计算可以从图中看出:

r-l是ViewGroup固定的宽度、减去子View的宽度再除2即子View到ViewGoup左边的距离。

  • 子View left距离父ViewGoup Top的轴距离 =(r-l-oneChildViewWidth)/2

  • 子View top距离父ViewGoup Top的距离 =(b-t-oneChildViewHeight)/2

72a3e66be6c92b042ffd5d55a4d17b5e.gif

onLayout中我们进行摆放,摆放中子View所摆放的参考系是父ViewGoup左上角。代码如下:

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)
    }

运行结果:我们可以看到不管宽高都可以容纳其中

0018692b0ce24dc3a01db9bc954464fb.gif

当然我们的可伸展ViewGroup在这里也不会随便设置过分的宽高吧。所以代码里面对于测量的计算和摆放都简单的计算,展开我们还没有进行计算,展开的宽度即所有子布局的宽度和margin以及pading和:

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)

        }
    }

设置点击时间控制动画执行等就不啰嗦了

/**
 *
 *  ┌─────────────────────────────────────────────────────────────┐
 *  │┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐│
 *  ││Esc│!1 │@2 │#3 │$4 │%5 │^6 │&7 │*8 │(9 │)0 │_- │+= │|\ │`~ ││
 *  │├───┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴───┤│
 *  ││ Tab │ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │{[ │}] │ BS  ││
 *  │├─────┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴─────┤│
 *  ││ Ctrl │ A │ S │ D │ F │ G │ H │ J │ K │ L │: ;│" '│ Enter  ││
 *  │├──────┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴────┬───┤│
 *  ││ Shift  │ Z │ X │ C │ V │ B │ N │ M │< ,│> .│? /│Shift │Fn ││
 *  │└─────┬──┴┬──┴──┬┴───┴───┴───┴───┴───┴──┬┴───┴┬──┴┬─────┴───┘│
 *  │      │Fn │ Alt │         Space         │ Alt │Win│   HHKB   │
 *  │      └───┴─────┴───────────────────────┴─────┴───┘          │
 *  └─────────────────────────────────────────────────────────────┘
 * 版权:渤海新能 版权所有
 *
 * @author feiWang
 * 版本:1.5
 * 创建日期:1/21/21
 * 描述: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) {
    //初始化第一次宽度默认不写为0

    private var childRota: Boolean=false

    /**
     * lt_width
     * LinearLayout总共的宽度
     */
    var lt_width=0

    /**
     * 可点击关闭和打开的子view
     * if gravaty is left that firstChild is oneChildView
     * if gravaty is right that endingChild is oneChildView
     * if top
     * if end
     */
    lateinit var oneChildView:View
    /**
     * 选择状态
     */
    var animalStar=false

    /**
     * 动画定义
     */
    var scaleX :ValueAnimator?=null

    /**
     * 动画值跟新布局长度
     */
    var animalValue=0
    /**
     *
     */
    var animalDuration=0

    /**
     * 标记来控制动画的执行方向
     */
    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() {

        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.animatedValue as 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)

        }
    }


}

自定义-从右向左可伸展的LineaLayout

我们会发现测量完成之后第一个子View所在的父容器任意旋转都有足够的空间,但是问题来了,子View并不是摆放在中间。我们只是给了子View足够的空间,但是并没有将其放在这个空间中间,接下来我们进行子View的摆放。

20f1be2e7ffc6bea96cbc218980ea629.jpeg

同样我们会发现第二个、第三个、第四个的left只是和第一个相差R(对角线)的整数倍。如下代码可得:

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)

            }

这里我们可以看到不管如何旋转都不会有所遮挡子View任何部位.

bd19b4987bbb154ae181a8d46dcd90ee.gif

到这里我们似乎很完美的结局了旋转带来的问题.我们将视图放在右侧呢?会是我们想要的效果么,可想而知我们的第一个子view在展开时候会跑到左边.而不是原来的位置.如下所示.它会从最后跑到最前面去:

c42958b9fd197891e5e02bce6894d1ac.png

效果是按钮一直在最后.所以第一个View默认我们设置到最后一个按钮的位置,相反展开时候需要将最后一个按钮位置放置在第一个位置作为交换.代码如下:

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

/**
 *
 *  ┌─────────────────────────────────────────────────────────────┐
 *  │┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐│
 *  ││Esc│!1 │@2 │#3 │$4 │%5 │^6 │&7 │*8 │(9 │)0 │_- │+= │|\ │`~ ││
 *  │├───┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴───┤│
 *  ││ Tab │ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │{[ │}] │ BS  ││
 *  │├─────┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴─────┤│
 *  ││ Ctrl │ A │ S │ D │ F │ G │ H │ J │ K │ L │: ;│" '│ Enter  ││
 *  │├──────┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴────┬───┤│
 *  ││ Shift  │ Z │ X │ C │ V │ B │ N │ M │< ,│> .│? /│Shift │Fn ││
 *  │└─────┬──┴┬──┴──┬┴───┴───┴───┴───┴───┴──┬┴───┴┬──┴┬─────┴───┘│
 *  │      │Fn │ Alt │         Space         │ Alt │Win│   HHKB   │
 *  │      └───┴─────┴───────────────────────┴─────┴───┘          │
 *  └─────────────────────────────────────────────────────────────┘
 * 版权:渤海新能 版权所有
 *
 * @author feiWang
 * 版本:1.5
 * 创建日期:1/21/21
 * 描述: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) {
    //初始化第一次宽度默认不写为0

    private var childRota: Boolean=false

    /**
     * lt_width
     * LinearLayout总共的宽度
     */
    var lt_width=0

    /**
     * 可点击关闭和打开的子view
     * if 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
    /**
     * 选择状态
     */
    var animalStar=false

    /**
     * 动画定义
     */
    var scaleX :ValueAnimator?=null

    /**
     * 动画值跟新布局长度
     */
    var animalValue=0
    /**
     *
     */
    var animalDuration=0

    /**
     * 标记来控制动画的执行方向
     */
    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() {

        oneChildView.setOnClickListener {
            if(scaleX==null) {
                scaleX = ObjectAnimator.ofFloat(oneChildViewWidth.toFloat(), lt_width.toFloat())
            }
            scaleX?.duration = animalDuration.toLong()
            scaleX?.addUpdateListener { animation ->
                animalValue  = MeasureSpec.makeMeasureSpec((animation.animatedValue as 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(childRota){//旋转的子布局都需要重新排序一下
                //首先进行摆放
                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)

                }
                //进行交换位置
                val starchild_m=getChildAt(0)
                val endchild_m=getChildAt(childCount-1)
                //附值避免执行完成{@see #onMeasure()}被修改
                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{
            //这里简化应该求各个子View中最高的一个
            heightMeasureSpec=MeasureSpec.makeMeasureSpec(childViewHeight, MeasureSpec.AT_MOST)
        }
        if(!animalStar){
            //如果子View有旋转
            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)

        }
    }
}

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>

最终效果:由于用投屏软件导致有卡顿,手机上还事很丝滑的.

3f30c2f5c7241a75f3c9f1d44c81c5a3.gif

/   总结   /

自定义每个人都可以,测量+摆放+绘制高配一些数学计算和动画就能制作出很好的交互View,需要的是动手,坚持,耐心。

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

Android 13 Developer Preview一览

PermissionX 1.7发布,全面支持Android 13运行时权限

欢迎关注我的公众号

学习技术或投稿

47e8c822b02bafbbe157474baa7f5972.png

5724d77065d24a2d5d838a4d1c29a823.jpeg

长按上图,识别图中二维码即可关注

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值