DP utitilies on Int and Float
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).
I think we would call these toDp as conversion factory extensions are prefixed with "to".
Ah, that's fine, updating the description to to. 👍
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.
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().
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()
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
Contextor (more common) aView, ex:View().apply { translationX = toPixelFromDip(2) }
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 ?
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.
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
)
}
This wouldn't work with multiple displays (virtual or physical).
In my opinion, this is the best implementation.
fun Number.dpToPx(): Int = (toFloat() * Resources.getSystem().displayMetrics.density + .5f).toInt()
@johanneslaubermoovel This doesn't work with negative numbers. Also you are relying on display metrics that might not match the display.
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?
Resources will use the DisplayMetrics of the Display the Resources came from.
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().
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).
@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)
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.
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)
@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()
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.
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 .
@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()
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?)