android-ktx
android-ktx copied to clipboard
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
Context
or (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?)