android-ktx icon indicating copy to clipboard operation
android-ktx copied to clipboard

DP utitilies on Int and Float

Open imminent opened this issue 7 years ago • 24 comments

Calculating dimensions, such as, SP and DP require the DisplayMetrics, so you can't do something too simple without - say - adding Extensions inside of Activity and such directly. But you can still make some progress similar to the Duration extensions doing something like this:

/**
 * Applies density dimensions to the [Int]
 */
fun Int.toDp(displayMetrics: DisplayMetrics) = toFloat().toDp(displayMetrics).toInt()

/**
 * Applies density dimensions to the [Float]
 */
fun Float.toDp(displayMetrics: DisplayMetrics) = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, displayMetrics)

Which would be called like 9.toDp(displayMetrics).

imminent avatar Feb 05 '18 20:02 imminent

I think we would call these toDp as conversion factory extensions are prefixed with "to".

JakeWharton avatar Feb 05 '18 20:02 JakeWharton

Ah, that's fine, updating the description to to. 👍

imminent avatar Feb 05 '18 20:02 imminent

Just curious, why is it applied on Int & Float instead of on DisplayMetrics ike displayMetrics.toDp(9)?

That way you can also name the param "pixel" so it's less confusing.

nohitme avatar Feb 05 '18 20:02 nohitme

DisplayMetrics is merely a tool needed to execute the conversion, the parameter that matters is the value. In an ideal world it would be 9.toDp().

imminent avatar Feb 05 '18 20:02 imminent

I am not sure if it should be treated/interpreted as a conversion. mostly you simply want do pass a value in dp to a method which takes pixels. so 9.toDp() I would assume converting 9px to Ndp but what we want is to be get the value 9 get treated as dp and therefore I would prefer 9.asDp()

daberni avatar Feb 05 '18 21:02 daberni

I wrote this utility:

fun Context.toPixelFromDip(value: Float) = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, resources.displayMetrics)
fun View.toPixelFromDip(value: Float) = context.toPixelFromDip(value)

Pro:

  • toPixelFromDip() is very clear the origin and the destination
  • It's handy when you call this method from a Context or (more common) a View, ex: View().apply { translationX = toPixelFromDip(2) }

omarmiatello avatar Feb 05 '18 22:02 omarmiatello

Wouldn't accessing system Resources just be sufficiet? So we can have something like:

val Int.dp: Int // or just return Float here
    get() = (this * Resources.getSystem().displayMetrics.density).toInt()

and use it in place like val side = 42.dp ?

lub0s avatar Feb 06 '18 08:02 lub0s

I don't think so. Your application resources may have a different density. Maybe not now, but you don't know in future. Also seems correct take it from application context.

omarmiatello avatar Feb 06 '18 08:02 omarmiatello

What about using Number?

fun Number.toDp(context: Context): Float {
    if (this == 0) return 0f

    return TypedValue.applyDimension(
        TypedValue.COMPLEX_UNIT_DIP, 
        this.toFloat(), 
        context.resources.displayMetrics
    )
}

Kevinrob avatar Feb 06 '18 08:02 Kevinrob

This wouldn't work with multiple displays (virtual or physical).

romainguy avatar Feb 06 '18 08:02 romainguy

In my opinion, this is the best implementation.

fun Number.dpToPx(): Int = (toFloat() * Resources.getSystem().displayMetrics.density + .5f).toInt()

johanneslaubermoovel avatar Feb 06 '18 08:02 johanneslaubermoovel

@johanneslaubermoovel This doesn't work with negative numbers. Also you are relying on display metrics that might not match the display.

romainguy avatar Feb 06 '18 08:02 romainguy

I am against using Number as input since only Int and Float are the common types representing dimensions in Android.

This wouldn't work with multiple displays (virtual or physical).

@romainguy this just blew my mind. Say a secondary display is connected, which DisplayMetrics then would the Resources refers to?

hanggrian avatar Feb 28 '18 02:02 hanggrian

Resources will use the DisplayMetrics of the Display the Resources came from.

romainguy avatar Feb 28 '18 17:02 romainguy

I personally would go with the following methods:

fun Number.toDp(displayMetrics: DisplayMetrics? = null): Float = this.toFloat() / ((displayMetrics ?: Resources.getSystem().displayMetrics).densityDpi / 160f)
fun Number.toPx(displayMetrics: DisplayMetrics? = null) : Float = this.toFloat() * ((displayMetrics ?: Resources.getSystem().displayMetrics).densityDpi / 160f)

displayMetrics is an optional parameter in case you have multiple displays. If not, you have the option of a simple 8.px() or 12.px().

lukewaggoner-zz avatar Mar 05 '18 23:03 lukewaggoner-zz

This seems error prone. The DisplayMetrics should be mandatory. BTW no need to / 160.0f, just use DisplayMetrics.density instead (and you need to properly round and handle negative values correctly).

romainguy avatar Mar 06 '18 18:03 romainguy

@JakeWharton What about using these converters?

fun Number.toDp(displayMetrics: DisplayMetrics): Float = this.toFloat() / displayMetrics.density
fun Number.toSp(displayMetrics: DisplayMetrics): Float = this.toFloat() / displayMetrics.scaledDensity
fun Number.toPx(displayMetrics: DisplayMetrics, fromSp: Boolean = false): Float
        = this.toFloat() * (if (fromSp) displayMetrics.scaledDensity else displayMetrics.density)

iglaweb avatar Mar 24 '18 20:03 iglaweb

These are incorrect, toPx in particular does not round properly (which needs to take negative numbers into account). I would also prefer to not have a fromSp boolean, but a separate method.

romainguy avatar Mar 25 '18 18:03 romainguy

If you allow arch components, then you can have an onCreateProperty (which is like an Android-specific lateinit var) and define extensions on Activity and Fragment:

/**
 * Initializes a property with the DP value
 * @receiver An [Activity] that is a [LifecycleOwner]
 * @return [Float] representation of the DP value
 * @see [onCreate]
 */
fun <T> T.dp(value: Float) where T : Activity, T : LifecycleOwner = onCreate {
    value.toDp(resources.displayMetrics)
}

/**
 * Initializes a property with the DP value
 * @receiver A [Fragment] that is a [LifecycleOwner]
 * @return [Int] representation of the DP value
 * @see [onCreate]
 */
fun <T> T.dp(value: Int) where T : Fragment, T : LifecycleOwner = onCreate {
    value.toDp(resources.displayMetrics)
}

// In Activity
val margins by dp(5)

imminent avatar Mar 29 '18 17:03 imminent

@romainguy I refined concrete types for these density functions, could you explain why they are not applicable for negative values? (tested myself)

fun Int.toDp(displayMetrics: DisplayMetrics): Float = this.toFloat() / displayMetrics.density
fun Int.toSp(displayMetrics: DisplayMetrics): Float = this.toFloat() / displayMetrics.scaledDensity
fun Float.spToPx(displayMetrics: DisplayMetrics): Int =
        (this * displayMetrics.scaledDensity).roundToInt()
fun Float.dpToPx(displayMetrics: DisplayMetrics): Int =
        (this * displayMetrics.density).roundToInt()

iglaweb avatar Mar 30 '18 07:03 iglaweb

roundToInt() rounds toward the nearest integer (towards positive infinity when there's a tie), so it won't round in the right direction for negative numbers. For instance 0.5f rounds to 1, but -0.5f rounds to 0.

romainguy avatar Mar 30 '18 15:03 romainguy

What would make it error prone? The only reason you'd ever need to specify displayMetrics would be if you have more than one display. That's never happened to me.

On Tue, Mar 6, 2018, 11:06 AM Romain Guy [email protected] wrote:

This seems error prone. The DisplayMetrics should be mandatory. BTW no need to / 160.0f, just use DisplayMetrics.density instead (and you need to properly round and handle negative values correctly).

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/android/android-ktx/issues/132#issuecomment-370873344, or mute the thread https://github.com/notifications/unsubscribe-auth/AGAblJmBErnf4IzdoB6M72YRpRZPWRwsks5tbtAfgaJpZM4R6ERs .

lukewaggoner-zz avatar Apr 07 '18 20:04 lukewaggoner-zz

@romainguy possible fix, round should be ok

fun Float.spToPx(displayMetrics: DisplayMetrics): Int =
        (this * displayMetrics.scaledDensity).round()
fun Float.dpToPx(displayMetrics: DisplayMetrics): Int =
        (this * displayMetrics.density).round()
private fun Float.round(): Int = (if(this < 0) ceil(this - 0.5f) else floor(this + 0.5f)).toInt()

iglaweb avatar Apr 15 '18 12:04 iglaweb

How about a small DSL that would make it possible to write stuff like:

view.context.pixelConversions {
    outRect.left = 80.dp
    outRect.right = 20.dp
}

Using the idea from the first post, that would be:

fun Context.pixelConversions(block: ConversionContext.() -> Unit) {
    block(ConversionContext(resources.displayMetrics))
}

class ConversionContext(private val displayMetrics: DisplayMetrics) {
    val Int.dp get() = toFloat().dp.toInt()

    val Float.dp get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, displayMetrics)
}

After trying to apply it in a different setting, I'm not sure how useful this idea actually is 🤔

(I forgot this repo is unused now, is this ticket tracked on google issue tracker?)

arekolek avatar Oct 11 '18 21:10 arekolek