Various Square- or Fixed-ratio layouts and views

These are various snippets that can be used to create views that have a fixed ratio.

Note that you can often use a ContraintLayout instead, and apply a ratio on its children:

<androidx.constraintLayout.widget.ConstraintLayout ... >
    <TextView
        android:text="I will be square"
        app:layout_constraintDimensionRatio="H,1:1"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        ...
        />
</androidx.constraintLayout.widget.ConstraintLayout>

SquareLayout.kt

package com.pixplicity.ui.view.

import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import kotlin.math.min

/**
 * FrameLayout that forces the height to match the width
 */
class SquareLayout : FrameLayout {
    constructor(context: Context) : this(context, null, 0)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // Return height as width to force square
        val width = MeasureSpec.getSize(widthMeasureSpec)
        val height = MeasureSpec.getSize(heightMeasureSpec)
        val size = min(width, height)
        val makeMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY)
        super.onMeasure(makeMeasureSpec, makeMeasureSpec)
    }

}

RatioLayout.kt

An improved version can take any ratio through the XML attributes.

RatioLayout.kt

package com.pixplicity.example.views

import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import com.pixplicity.example.R


/**
 * Makes the height match the width, using the set ratio
 */
class RatioLayout(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : FrameLayout(
    context,
    attrs,
    defStyleAttr
) {

    var ratio = 1f
        set(value) {
            field = value
            requestLayout()
        }

    constructor(context: Context) : this(context, null, 0)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)

    init {
        context.theme.obtainStyledAttributes(
            attrs,
            R.styleable.RatioLayout,
            0, 0
        ).apply {
            try {
                ratio = getFloat(R.styleable.RatioLayout_ratio, 1f)
            } finally {
                recycle()
            }
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // Return height as width to force square
        val width = MeasureSpec.getSize(widthMeasureSpec)
        val height = (width.toFloat() / ratio).toInt()
        //val size = min(width, height)
        val wSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
        val hSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
        super.onMeasure(wSpec, hSpec)
    }

}

res/values/attr.xml

<resources>
    <declare-styleable name="RatioView">
        <attr name="ratio" format="float" />
    </declare-styleable>
</resources>

Now you can use it like so:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:custom="http://schemas.android.com/apk/res/com.pixplicity.example.views">
 <com.pixplicityexample.views.RatioLayout
     app:ratio="1" />
</LinearLayout>

RatioImageView.kt

ImageView that takes a predefined width-to-height ratio. The ratio is set programmatically, not in the XML.

package com.pixplicity.ui.view

import android.content.Context
import android.support.v7.widget.AppCompatImageView
import android.util.AttributeSet
import android.view.View
import android.widget.Checkable
import android.widget.ImageView
import kotlin.math.min


/**
 * Makes the height match the width, using the set ratio
 */
class RatioImageView : ImageView {

    private var ratio = 1f

    constructor(context: Context) : this(context, null, 0)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    /**
     * w:h
     */
    fun setRatio(r: Float) {
        ratio = r
        requestLayout()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // Return height as width to force square
        val width = MeasureSpec.getSize(widthMeasureSpec)
        val height = (width.toFloat() / ratio).toInt()
        //val size = min(width, height)
        val wSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
        val hSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
        super.onMeasure(wSpec, hSpec)
    }

}

SquareImageView.kt

Checkable, square ImageView.

package com.pixplicity.ui.view

import android.content.Context
import android.support.v7.widget.AppCompatImageView
import android.util.AttributeSet
import android.view.View
import android.widget.Checkable
import android.widget.ImageView
import kotlin.math.min

class SquareImageView : AppCompatImageView, Checkable {

    companion object {
        private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked)
    }

    private var checked = false

    constructor(context: Context) : this(context, null, 0)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // Return height as width to force square
        val width = MeasureSpec.getSize(widthMeasureSpec)
        val height = MeasureSpec.getSize(heightMeasureSpec)
        val size = min(width, height)
        val makeMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY)
        super.onMeasure(makeMeasureSpec, makeMeasureSpec)
    }

    override fun onCreateDrawableState(extraSpace: Int): IntArray {
        val drawableState = super.onCreateDrawableState(extraSpace + 1)
        if (isChecked) {
            View.mergeDrawableStates(drawableState, CHECKED_STATE_SET)
        }
        return drawableState
    }

    override fun isChecked(): Boolean = checked

    override fun toggle() {
        checked = !checked
    }

    override fun setChecked(checked: Boolean) {
        this.checked = checked
    }

}