مهندس نرمافزار
مهندس روابط توسعهدهنده
آمادهاید تا میانای کاربری Android خود را از «کار میکند» به «یک اثر هنری است» ارتقا دهید؟
برای گشتوگذار در جدیدترین قابلیتهای گرافیکی در Jetpack Compose به ما بپیوندید. به شما نشان خواهیم داد که چگونه جلوههای دیداری جدید بسازید، از گرادیانهای مش دلپذیر گرفته تا محو کردنهای پیشرونده، و استفاده از ویژگیهای گرافیکی قدرتمند جدید در Android. همچنین به شما نشان خواهیم داد که چگونه عملکرد ویژگیهای گرافیکی خود را بدون فدا کردن عمر باتری یا نرخ فریم بهبود بخشید.
PolygonShape و MorphPolygonShape را برای هندسههای سفارشی معرفی میکند. MorphPolygonShape پیشرفت را درطول مرحله وضوح طرح کلی میخواند و شکلها را فریمبهفریم بدون راهاندازی بازترکیب بهروز میکند.RenderNode.setBackdropRenderEffect را برای نمونهبرداری از محتوای پسزمینه معرفی میکند. توسعهدهندگان میتوانند جلوههای پساویز را بههم زنجیر کنند تا جلوههای شیشه مات یا ذرهبین ایجاد کنند.graphicsLayer قرار دهید.آوانویسی
سلام به همه، به جلسه ما خوش آمدید، ارتقای برنامههای شما با جدیدترین ویژگیهای گرافیکی در Compose و Android.
نام من ربکا فرانکس است و امروز در صحنه به من میپیوندد--
من آیوش چاودهاری هستم.
سلام، من راهول ری هستم.
ما شاهد روندی نوظهور در نحوه جابهجا کردن مرزهای ممکن در Android و Compose توسط شما هستیم، که بااستفاده از هوش مصنوعی ایدههای شگفتانگیزتان را واقعاً به واقعیت تبدیل میکنید.
و دوست داریم برنامههایی را ببینیم که حس ممتاز، بومی، و لذتبخش بودن را القا میکنند.
میدانیم برنامههایی که حس خوبی به کاربر میدهند و بهطور مداوم رتبهبندیهای عالی از کاربران دریافت میکنند، عموماً از جدیدترین تغییرات میانای برنامهسازی کاربردی در سطح سیستم بهره میبرند، بین سطوح با عناصر مشترک و محو کردن تداوم دارند، بهطور لذتبخشی با انواع ابعاد سازگار میشوند، عملکرد خوبی دارند، کاربران را در حدس زدن رها نمیکنند، و مرزهای آنچه را که با دستگاه شما ممکن است جابهجا میکنند.
در این گفتگو، برخیاز جدیدترین ویژگیهایی را که به شما کمک میکند از Android بهره ببرید به شما نشان میدهیم، ازجمله خوشحال کردن کاربران با گرادیانهای مش، فعال کردن پشتیبانی از گستره رنگی وسیع، انتقالهای لذتبخش با اشکال چندضلعی، موارد جدید در محو کردن، و درنهایت، نحوه فکر کردن درباره عملکرد هنگام افزودن ویژگیهای گرافیکی جدید به برنامههایتان را پوشش خواهیم داد.
اما درطول صحبت امروز، برخیاز میاناهای برنامهسازی کاربردی جدید را در سطوح مختلف پشته پوشش خواهیم داد.
درحالیکه جعبهابزار میانای کاربر نمای قدیمی اکنون در حالت نگهداری است، ما همچنان در موتورهای رندر سختافزار گرافیکی Android که Jetpack Compose را تقویت میکنند، سرمایهگذاری میکنیم.
میتوانید گرافیک در Android را در سه لایه مختلف تصور کنید: میاناهای برنامهسازی کاربردی Compose در لایه اول قرار دارند و همه میاناهای برنامهسازی کاربردی گرافیک جدید و سطح بالا در این لایه قرار دارند، مانند رنگآمیزهای گرادیان مش، شکلهای چندضلعی، و میاناهای برنامهسازی کاربردی محو کردن تدریجی.
و سپس لایه میانی را داریم که در آن DrawScope، گرههای اصلاحکننده ترسیم، و AGSL در Compose به شما امکان میدهند جلوههای سفارشی بسازید.
اما درنهایت، پلاتفرم Android را داریم و Compose همیشه میتواند به Canvas بومی دسترسی داشته باشد و از اصول اولیه رندرینگ سیستمعامل جدید درست در داخل اصلاحکنندههای Compose شما استفاده کند.
و چند نمونه از این کار را امروز به شما نشان خواهیم داد.
بیایید شروع کنیم.
بنابراین ابتدا، بیایید به گرادیانهای مش، یک API جدید معرفیشده در Compose 1.12، نگاه کنیم.
قبلاً گرادیانهای خطی داشتیم، قبلاً گرادیانهای خطی، جاروبی، و شعاعی داشتیم که برای انتقالهای ساده عالی هستند، اما اگر ترکیبهای چندرنگی غنی مانند پسزمینههای محیطی یا جلوههای نورافکن میخواهید، به شبکه دوبعدی از سیستم نیاز دارید که در آن، موقعیتها و رنگها بتوانند در فضا ترکیب شوند.
اکنون، برای ایجاد گرادیان مشبک مانند این، ابتدا مشخص کنید که چند ردیف و ستون از وصلهها را میخواهید.
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
}
}سپس در داخل لامبدا دنبالهدار، setVertex را فراخوانی کنید و یک انحراف نرمالشده در محدوده 0F تا 1F، همراه با یک رنگ برای هر تقاطع شبکه اختصاص دهید.
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
}
}و سپس برای پویانمایی آن رأسها، جابهجاییهای پویانماییشده برای هشت رأس اضافه کنید.
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
// ....
setVertex(1, 1, Offset(0.2f, 0.4f) + Offset(animatedOffset, animatedOffset), coral)
// ....
setVertex(3, 0, Offset(0.0f, 1.0f), sunshine)
// ....
}
}
Box(
modifier = modifier.padding(32.dp)
.safeContentPadding()
.aspectRatio(16 / 9f)
.fillMaxWidth()
.paint(gradientPainter)
)وقتی نقاش شما تعریف شد، بهراحتی آن را با هر ترکیبپذیری ازطریق اصلاحکننده رنگآمیزی فراخوانی کنید.
اکنون در برنامه JetChat، ویژگیای اضافه کردهایم که وقتی روی پیامی دوضربه میزنید، آن را میپسندد.
برای ساختن این، یک توری ۲ در ۳ راهاندازی میکنیم و ایستگاههای رنگی را در سراسر مؤلفه قرار میدهیم.
MeshGradientPainter(
rows = 2,
columns = 3,
hasBicubicColor = true,
) {
// Row 0 (top edge, y = 0.0f)
setVertex(0, 0, Offset(0.0000f, 0.0000f), Color(0xFFFF6B6B))
setVertex(0, 1, Offset(0.3300f, 0.0000f), Color(0xFFFFD6D6))
setVertex(0, 2, Offset(0.6700f, 0.0000f), Color(0xFFE8C3FF))
setVertex(0, 3, Offset(1.0000f, 0.0000f), Color(0xFFE396FF))
// ... remaining vertices for Rows 0, 1, and 2
}توجه کنید که در اینجا hasBicubicColor را روی درست تنظیم میکنیم.
بهطور پیشفرض، رنگها بهصورت خطی در مثلثها درونیابی میشوند که گاهی اوقات میتواند تکههای قابلمشاهدهای باقی بگذارد.
با روشن کردن این گزینه، سایهزن به درونیابی دومکعبی هموار تغییر میکند تا رنگها بتوانند بهطور یکپارچه در یکدیگر ذوب شوند.
حال اگر بخواهید درخشش محیطی پویانماییشدهای مثل این در پشت چارگوش ورودی بسازید چه؟
برای رسیدن به جلوهای مانند این با گرادیان مش، به دو چیز نیاز دارید.
ابتدا، یک حرکت ارگانیک نرم برای رئوس داخلی و راهی برای نرم کردن لبههای بیرونی تا درنهایت یک مستطیل سخت در اطراف کارت نداشته باشید.
بنابراین، کار را با تنظیم rememberInfiniteTransition با دو زاویه فاز که با مدتهای کمی متفاوت اجرا میشوند شروع میکنیم تا حرکت بهجای مکانیکی، طبیعی بهنظر برسد.
val transition = rememberInfiniteTransition(label = "userInputGlowMesh")
val phase1 by transition.animateFloat(
initialValue = 0f,
targetValue = (2 * PI).toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 2800, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "glowPhase1",
)
val phase2 by transition.animateFloat(
initialValue = 0f,
targetValue = (2 * PI).toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 4200, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "glowPhase2",
)در اینجا، ما رئوس داخلی را متحرک میکنیم و حلقههای بیرونی، حلقه بیرونی رئوس را با لبههایی با آلفای ۰ سنجاق میکنیم.
چون مرز بیرونی ثابت و شفاف است، درخشش بهطور طبیعی محو میشود.
و درنهایت، این جابهجاییهای پویانماییشده را به MeshGradientPainter خودمان میدهیم.
MeshGradientPainter(
rows = 6,
columns = 5,
hasBicubicColor = true,
) {
for (r in 0..6) {
for (c in 0..5) {
val offset = if (r in 1..5 && c in 1..4) {
val ampX = when (r) {
1, 2 -> 0.048f
3 -> 0.042f
4 -> 0.028f
else -> 0.036f
}
val ampY = when (r) {
1, 2 -> 0.028f
3 -> 0.020f
4 -> 0.022f
else -> 0.016f
}
val d = orbit(c, r, ampX, ampY)
Offset(
x = (baseU + d.x).coerceIn(0.04f, 0.96f),
y = (baseV + d.y).coerceIn(0.05f, 0.95f),
)
} else {
Offset(baseU, baseV)
}
setVertex(r, c, offset, gridColors[r][c])
}
}
}شیب توری که همینالان دیدید بااستفاده از میانای برنامهسازی کاربردی drawMesh در Android 14 و بالاتر ترسیم شده است.
پس بیایید با ساختن جلوهای از صفر، نگاهی دقیقتر به این میانای برنامهسازی کاربردی بیندازیم.
برای صفحه خوشامدگویی JetChat، میخواهیم مسیری را ترسیم کنیم که شبیه روبان باشد.
به دو چیز در اینجا توجه کنید: پهنای قلمضربه ثابت نیست و رنگ در امتداد هر قلمضربه مسیر حرکت میکند.
بیایید این را تجزیه کنیم و بفهمیم که واقعاً برای ساختن این جلوه به چه چیزی نیاز داریم.
برای هر پیکسل، باید تصمیم بگیریم که آن را بکشیم یا نه و از چه رنگی استفاده کنیم.
برای فهمیدن این موضوع، به دو فاصله نیاز داریم.
مقدار اول نشان میدهد که پیکسل چقدر از مرکز خط فاصله دارد.
فاصله دوم نشان میدهد که پیکسل چقدر درطول مسیر از ابتدا تا انتها قرار دارد.
این به ما میگوید کدام رنگ را از گرادیان انتخاب کنیم و ترکیب کردن کمی از مقدار اول باعث میشود رنگ در سراسر عرض منحنی شود.
پس آیا میتوانیم بااستفاده از فقط مسیر و رنگ، گرادیانی را درطول مسیر رسم کنیم؟
بنابراین، بله، میتوانیم.
این کار مستلزم مسطح کردن مسیر به فهرستی از خطوط مستقیم کوتاه است که فاصله درطول مسیر را نیز ارائه میدهد.
val lines = remember {
path.asAndroidPath().flatten(0.5f)
}
//... inside draw scope
lines.forEach { line ->
//...
drawLine(
brush = Brush.linearGradient(...),
start = Offset(line.start.x,
line.start.y),
end = Offset(line.end.x, line.end.y),
strokeWidth = 30f,
)
//...
}سپس هر خط را با گرادیان خودش رسم میکنیم.
این روش کار میکند، اما هر خط به یک فراخوانی ترسیم جداگانه و یک شیء رنگ نیاز دارد و کل مسیر همچنان یک عرض واحد دارد.
گزینه دیگر سایهزن زمان اجرا است، اما این گزینه فقط موقعیت پیکسلها را در محدوده ظرف دریافت میکند.
این به ما نمیگوید که برای کشیدن این روبان به چه چیزی نیاز داریم.
و مثلاً نمیتوانیم آن را بهراحتی از موقعیت پیکسلها محاسبه کنیم.
«ترسیم مش» سایهزن رأس را برای این کار به ما میدهد.
خب، مش چیست؟
مش از مثلثهایی ساخته شده است که در هر گوشه آن دادههایی وجود دارد و هر گوشه آن رأس نامیده میشود.
هر رأس حاوی سابقه کوچکی از دادهها است.
معمولاً آن رکورد جایگاهی را حفظ میکند، اما لزوماً اینطور نیست.
سایهزن رأس آن رکورد را میخواند و موقعیت را از آن محاسبه میکند.
اینجا، این مسیری است که یک مثلث طی میکند: سایهزن رأس برای هر گوشه اجرا میشود، موقعیت گوشهها را محاسبه میکند، و هر مقداری را که بخواهیم برای هر پیکسل منتقل کنیم، منتقل میکند.
سپس شطرنجیساز هر پیکسلی را که توسط آن مثلث پوشانده شده است پیدا میکند و مقادیر را از آن سه گوشه برای هر پیکسل درونیابی میکند.
درنهایت، سایهزن قطعه برای آن پیکسلها اجرا میشود، از آن مقادیر میانیابیشده استفاده میکند، و رنگ پیکسلهای محاسبهشده را محاسبه میکند.
بیایید ببینیم چگونه میتوانیم سایهزن رأس و قطعه را با یک مثال سادهتر بنویسیم، یک خط مستقیم با یک پالس که در امتداد آن حرکت میکند.
LineShaders.VERTEX
uniform float2 uStart;
uniform float2 uEnd;
uniform float uHalfWidth;
uniform float uTime;
Varyings main(const Attributes a) {
Varyings v;
float2 dir = uEnd - uStart;
float2 normal = normalize(float2(-dir.y, dir.x));
float along = a.corner.x * length(dir);
float pulse = (a.corner.x - uTime) / 0.18;
float radius = uHalfWidth * (1.0 + 0.6 * exp(-pulse * pulse));
float across = a.corner.y * (radius + 1.0);
v.position = uStart + dir * a.corner.x + normal * across;
v.stroke = float3(along, across, radius);
return v;
}سایهزن رأس یک مشخصه را بهعنوان ورودی میگیرد و یک متغیر را برمیگرداند.
مشخصهها دادههای مربوط به یک گوشه هستند.
اینجا فقط دو عدد وجود دارد: پیشرفت آن در امتداد خط، و اینکه در کدام طرف خط قرار دارد.
سپس متغیرها درونیابی میشوند و به سایهزن قطعه منتقل میشوند.
فاصله پیکسل را از ابتدا محاسبه میکنیم و برای پهنای قلمضربه، از زمان برای ایجاد پالس در شعاع استفاده میکنیم.
سپس آن شعاع را در ضلع گوشه ضرب میکنیم، که فاصله علامت را در سراسر خط به ما میدهد.
وقتی GPU بین این علامتها درونیابی میکند، خطوط مرکزی صفر میشوند.
درنهایت، موقعیت گوشهها را محاسبه میکنیم و همه این مقادیر را در متغیرهای قلمضربه ذخیره میکنیم و آنها را به سایهزن قطعه منتقل میکنیم.
برخلاف سایهزن زمان اجرا، سایهزن قطعه مش رنگی را ازطریق پارامتر برونداد میکند و درصورت تنظیم، موقعیتی را برای نمونهبرداری از رنگ برمیگرداند.
LineShaders.FRAGMENT
uniform float uLength;
layout(color) uniform float4 uStartColor;
layout(color) uniform float4 uEndColor;
float2 main(const Varyings v, out float4 color) {
float distance = abs(v.stroke.y);
float coverage = 1.0 - smoothstep(
v.stroke.z - 0.5, v.stroke.z + 0.5, distance);
float side = distance / v.stroke.z;
float4 ink = mix(uStartColor, uEndColor,
clamp(v.stroke.x / uLength + 0.4 * side * side, 0.0, 1.0));
color = ink * coverage;
return v.position;
}با گرفتن مقدار مطلق مختصات عرضی، فاصله پیکسل به پیکسل از خط مرکزی بهدست میآید.
ما آن فاصله را با شعاع مقایسه میکنیم تا پوشش را بهدست آوریم، و برای رنگ، در امتداد مسیر خط ترکیب میکنیم و گرادیان را کمی بهسمت لبه خم میکنیم.
آن پوشش را در پوشش ضرب کنید، این رنگ نهایی ما است.
در Compose، سایهزنها و چیدمان دادهها را با شیء MeshSpecification به هم متصل میکنیم.
val spec = remember {
MeshSpecification.make(
arrayOf(Attribute(TYPE_FLOAT2, 0, "corner")),
vertexStride = 8,
arrayOf(Varying(TYPE_FLOAT3, "stroke")),
LineShaders.VERTEX, LineShaders.FRAGMENT)
}ابتدا، مشخصه گوشه را بهعنوان دو عدد اعشاری که از بایت ۰ شروع میشوند اعلام میکنیم.
ازآنجاییکه دو عدد اعشاری ۸ بایت فضا میگیرند، هر گام گوشه در میانگیر ۸ است.
سپس، متغیرهای stroke را بهعنوان سه عدد اعشاری تعریف میکنیم و سایهزن رأس و قطعهای را که بهتازگی نوشتیم ارسال میکنیم.
ما بافر رأس را میسازیم.
fun lineCorners(segments: Int): ByteBuffer {
val buffer = ByteBuffer.allocateDirect((segments + 1) * 2 * 8)
.order(ByteOrder.nativeOrder())
for (i in 0..segments) {
val t = i / segments.toFloat()
buffer.putFloat(t).putFloat(-1f) // one side
buffer.putFloat(t).putFloat(1f) // the other side
}
return buffer.rewind() as ByteBuffer
}در هر مرحله از خط، پیشرفت را تکمیل میکنیم، پیشرفت را از ۰ تا ۱ محاسبه میکنیم، و دو گوشه را در میانگیر مینویسیم.
درنهایت، مشخصات و میانگیر را در یک شیء مش ترکیب میکنیم.
val mesh = Mesh(
spec,
mode = Mesh.TRIANGLE_STRIP,
vertexBuffer = corners,
vertexCount = (segments + 1) * 2,
bounds = RectF(0f, 0f, size.width, size.height)
)بوم Compose هنوز drawMesh را نمایش نمیدهد، بنابراین به nativeCanvas میرویم و مش را ترسیم میکنیم.
Box(Modifier.fillMaxSize().drawWithCache {
val mesh = Mesh(...)
onDrawBehind {
mesh.setFloatUniform("uTime", time.value)
drawIntoCanvas {
it.nativeCanvas.drawMesh(mesh,android.graphics.BlendMode.DST, paint)
}
}
})چون هندسه و پیکسل را کنترل میکنید، میتوانید بااستفاده از «رسم مش» هر جلوه بصری مبتنی بر هندسه را رسم کنید.
در اسناد توسعهدهنده درباره این میانای برنامهسازی کاربردی بیشتر بدانید.
در Compose نسخه ۱.۱۲، پشتیبانی از گاموتهای رنگی گسترده را معرفی کردیم.
خب، این واقعاً به چه معنا است؟
آیا میدانستید که صفحهنمایشها میتوانند طیف رنگی متفاوتی نسبتبه فضای رنگی سنتی sRGB تولید کنند؟
برای مثال، اگر به این تصویر نگاه کنید، میبینید که مثلث سفید محدوده رنگهای sRGB استاندارد است که اکثر صفحهنمایشها میتوانند نمایش دهند.
اما با صفحهنمایشهای جدیدتر، برخیاز آنها درواقع از طیف وسیعتری از رنگها پشتیبانی میکنند، مانند آنچه با مثلث زرد نشان داده شده است، برخی اشباعشدهتر و زندهتر.
اما وقتی رنگی را نشان میدهیم، درواقع آن را با شرح هر رأس آن مثلث نشان میدهیم.
وقتی به این مقایسه گرادیان نگاه میکنید، مهم است که بین گاموت نمایشگر و درونیابی گرادیانها تمایز قائل شوید.
در یک نمایشگر sRGB استاندارد یا احتمالاً این پروژکتور، صفحهنمایش نمیتواند رنگهای خارج از این گاموت sRGB را بهصورت فیزیکی ارائه دهد.
بنابراین، نمایش این ویژگی درحالحاضر روی صحنه واقعاً دشوار است.
بنابراین باید به من اعتماد کنید که رنگها در Android با پشتیبانی از گستره رنگی وسیع بهتر بهنظر میرسند.
در این مثال، آنچه درواقع در اینجا میبینید تأثیر درونیابی ایستگاههای رنگ در فضای رنگی وسیعتر است.
ترکیب رنگها در فضای رنگی گسترده یا خطی، مسیر ریاضی واقعی این گرادیان را تغییر میدهد و از این خاکستری شدن میانی و نواربندی خشنی که ممکن است بدون طیف رنگی گسترده رخ دهد، جلوگیری میکند.
با Compose نسخه ۱.۱۲، از مزیت ترکیب گرادیان هموارتر در هر نمایشگری و همچنین پشتیبانی از گستره رنگی وسیع واقعی هنگام اجرا در سختافزار پشتیبانیشده بهرهمند میشوید.
برای فعال کردن پشتیبانی صحیح در دستگاه، باید مطمئن شوید فعالیت یا پنجره شما برای فعال کردن حالت طیف رنگی گسترده تنظیم شده باشد.
و این کار را میتوانید در AndroidManifest انجام دهید.
<!-- AndroidManifest.xml -->
<activity
android:name=".NavActivity"
android:colorMode="wideColorGamut"
android:exported="true">
</activity>یا بهصورت برنامهنویسی در composable شما.
// Dynamic toggle in code
DisposableEffect(window) {
window?.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
onDispose {
window?.colorMode = ActivityInfo.COLOR_MODE_DEFAULT
}
}بدون تنظیم colorMode، پنجره در sRGB استاندارد ۸ بیتی باقی میماند و هر رنگ گستره رنگی وسیعی که توسط GPU ارائه میشود، بریده میشود.
بنابراین، اگر نمایشگر واقعاً نمیتواند از طیف رنگی گسترده پشتیبانی کند، مهم است که از رنگهای طیف رنگی گسترده استفاده نکنید.
برای صرفهجویی در مصرف باتری و بهبود عملکرد، باید طیف رنگی گسترده را فقط در فعالیتها یا مسیرهای کدی فعال کنید که واقعاً رسانه، عکس، یا گرافیک غنی نمایش میدهند.
این یعنی میتوانید در Compose رنگها را در فضاهای رنگی مختلف ایجاد کنید و اکنون این رنگها رعایت میشوند.
// 1. Display P3 Color (Wider gamut: greens & reds are more saturated) colorMode=wideColorGamut
val VividP3Red = Color(
red = 1.0f,
green = 0.05f,
blue = 0.05f,
alpha = 1.0f,
colorSpace = ColorSpaces.DisplayP3
)
// 2. Extended sRGB (Enables HDR values > 1.0f) use colorMode=hdr
val HdrNeonCyan = Color(
red = 0.0f,
green = 1.8f, // Peak brightness > 1.0 in extended gamut
blue = 2.0f,
alpha = 1.0f,
colorSpace = ColorSpaces.ExtendedSrgb
)
// 3. BT.2020 HLG (Broadcast HDR standard, supported API 34+) - use colorMode=hdr
val HdrSunsetOrange = Color(
red = 1.0f,
green = 0.45f,
blue = 0.1f,
alpha = 1.0f,
colorSpace = ColorSpaces.Bt2020Hlg
)میتوانید نوع colorSpace را که میخواهید رنگتان در آن ایجاد شود ارسال کنید، و اگر هیچ colorSpace ارسال نکنید، از sRGB استاندارد استفاده خواهد شد.
بهطور خاص برای بیتمپها، میتوانید بااستفاده از getColorSpace بررسی کنید که منبع بیتمپ چیست، و کتابخانههای بارگیری تصویر محبوب بهطور خودکار فضاهای رنگی بیتمپ صحیح را برای شما مدیریت میکنند.
باید از Bitmap.Config.HARDWARE برای حفظ اطلاعات رنگ استفاده کنید، زیرا پیکربندیای مانند 565 درواقع دقت را ازدست میدهد و نواربندی قابلمشاهدهای برای کاربران خواهد داشت.
برای کسب اطلاعات بیشتر درباره رنگها، میتوانید به سال ۲۰۱۷ برگردید و صحبتهای Romain Guy را درباره رنگ در Compose یا رنگ در Android تماشا کنید.
با Compose نسخه ۱.۱۳، پشتیبانی بومی از شکل چندضلعی را معرفی میکنیم.
این کار ساختن شکل سفارشی و عناصر واسط کاربر با ظاهر طبیعی را بسیار آسانتر میکند.
قبلاً اگر میخواستید هندسه پیچیدهای مثل این ترسیم کنید، باید از RoundedPolygon برای توصیف هندسهای استفاده میکردید که یک مسیر را خروجی میداد و سپس آن مسیر را به یک Compose shape API نگاشت میکردید.
fun cookiePolygon(): RoundedPolygon {
val vertices = FloatArray(32)
for (i in 0 until 16) {
val radius = if (i % 2 == 0) 1f else 0.84f
// …
}
return RoundedPolygon(
vertices,
rounding = CornerRounding(0.12f)
)
}برای مثال، ابتدا نسبتها را بااستفاده از RoundedPolygon تعریف میکنید و ما تنظیم میکنیم. در این قطعه کد، نقاط داخلی را روی ۸۴٪ از مرکز نسبتبه نقاط خارجی تنظیم میکنیم.
همچنین میتوانید گرد کردن گوشه را در اینجا اعمال کنید، اما این فقط منحنیهای ریاضی را توصیف میکند.
برای استفاده واقعی از این چندضلعی برای برش یا پسزمینه در Compose، باید یک پوشاننده Shape سفارشی مانند این بنویسید.
class RoundedPolygonShape(
polygon: RoundedPolygonShape
) : Shape {
// …
override fun createOutline(size: Size, layoutDirection: LayoutDirection,
density: Density,
): Outline {
path = polygon.toPath().asComposePath()
// …
matrix.scale(size.width / maxDimension, size.height / maxDimension)
matrix.translate(-bounds.left, -bounds.top)
// …
return Outline.Generic(path)
}
}و در داخل این قطعه createOutline، میتوانید ببینید که باید مسیر را ایجاد کنید و بهصورت دستی آنها را مقیاسبندی و ترجمه کنید.
اکنون با میانای برنامهسازی کاربردی جدید PolygonShape، دیگر به آن پوشاننده سفارشی Shape نیاز ندارید.
private val AvatarShape = PolygonShape {
val vertices = List(16) { i ->
val radius = size.minDimension / 2f * if (i % 2 == 0) 1f else 0.86f
val angle = i * PI.toFloat() / 8f
size.center + Offset(cos(angle), sin(angle)) * radius
}
polygon(vertices, rounding = CornerRounding(radius = 8.dp))
}
// …
Modifier.clip(AvatarShape)Because this is the dedicated Compose Shape builder, it receives the density and size directly out-of-the-box.
این یعنی میتوانید رأسهایتان را بهطور پویا محاسبه کنید و گردی گوشه را مستقیماً در dp یا حتی بهصورت درصد مشخص کنید.
پساز تعریف، فقط آن را به هر میانای برنامهسازی کاربردی که Shape میگیرد منتقل میکنید.
همچنین MorphPolygonShape API را معرفی میکنیم که به شما امکان میدهد بهطور یکپارچه بین شکلهای چندضلعی مختلف جابهجا شوید.
val recordShape = remember {
MorphPolygonShape(
start = PolygonShape.circle(),
end = PolygonShape.star(
numPoints = 9,
innerRadiusRatio = 0.8f,
outerRounding = CornerRounding(percent = 50),
),
) { shapeProgress.value }
}در این مثال، دکمه ضبطی داریم که وقتی با آن تعامل میکنید، از دایره به ستاره تبدیل میشود.
شما فقط شکل شروع، شکل پایان، و مقدار پیشرفت را ارائه میدهید.
MorphPolygonShape API مقدار پیشرفت را درطول فاز وضوح طرح کلی میخواند، یعنی نه درطول ترکیب مجدد، نه درطول ترکیببندی.
این یعنی وقتی شکل شما فریمبهفریم دگرگون میشود، بهسادگی به طرح کلی جدیدی تبدیل میشود بدون اینکه هرگز بازترکیبی را راهاندازی کند.
سپس، بیایید درباره محو کردن صحبت کنیم.
در چند نسخه اخیر، تلاش کردهایم پیادهسازی محو کردن را بسیار آسانتر و گویاتر کنیم.
هرگاه درباره محو کردن صحبت میکنیم، این اصلاحکننده رایج را میبینیم.
Modifier
.blur(
radiusX = 10.dp,
radiusY = 10.dp,
edgeTreatment = BlurredEdgeTreatment(
shape = RoundedCornerShape(8.dp),
),
)Compose از Android 12 این اصلاحکننده را داشته است.
این روش زمانی عالی است که میخواهید تاری یکنواختی داشته باشید، جایی که دقیقاً همان شعاع تاری در سراسر یک composable اعمال میشود.
اما طراحیهای میانای کاربر مدرن اغلب بهجای قطع ناگهانی، به انتقال نرم از نواحی واضح به نواحی محو نیاز دارند.
And that brings us to Progressive Blur, which is now an official API starting with Compose 1.13.
بهجای یک شعاع ثابت در کل ترکیبپذیر، اکنون میتوانید شدت محو کردن را در سراسر فضا تغییر دهید.
در اینجا مثالی از برنامه JetChat ما از صفحه نمایه کاربر آورده شده است.
اصلاحکننده محو کردن جدید، مشخصات شعاع محو کردن را میگیرد.
Modifier.blur {
blurRadiusSpec = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.5f, 0.dp),
BlurStop(1.0f, 32.dp),
)
)
edgeTreatment = BlurredEdgeTreatment.Unbounded
}در اینجا از گرادیان عمودی با دو توقف محوشدگی استفاده میکنیم، نیمه بالایی عکس را کاملاً واضح در 0 dp نگه میداریم و در لبه پایینی به محوشدگی 32 dp میرسیم.
توجه کنید که edgeTreatment را نیز بهعنوان نامحدود تنظیم کردهایم که اساساً به محو کردن میگوید پیکسل شفاف خارج از مرزهای قابلترکیب را نیز نمونهبرداری کند.
علاوهبر محو شدن عمودی، این مشخصات شعاع محو کردن چندین روش داخلی برای ایجاد محو کردن تدریجی به شما ارائه میدهد.
برای مثال، در سمت راست میتوانید گرادیان افقی را ببینید که شعاع شروع آن ۰ dp است و شعاع پایان آن با مقدار لغزنده تعیین میشود.
BlurRadiusSpec.horizontalGradient(
startRadius = 0.dp,
endRadius = sliderValue.dp,
)و در سمت راست، یک گرادیان شعاعی داریم که در مرکز آن، شعاع محوشدگی ۰ dp است و با حرکت به سمت fallOffRadius، که در این مورد ۳۰۰ dp است، محوشدگی بهآرامی تا ۲۴ dp افزایش مییابد.
BlurRadiusSpec.radialGradient(
center = center,
fallOffRadius = 300.dp,
startRadius = 0.dp,
endRadius = 24.dp,
)و اگر همه این گرادیانهای عمودی، شعاعی، و افقی برای شما کافی نیست، میتوانید همیشه محوشدگی تدریجی خود را بااستفاده از سایهزنهای سفارشی هدایت کنید.
val customShader = remember {
RuntimeShader(
"""
uniform float2 size;
uniform float time;
half4 main(float2 coords) {
// Diagonal wave mask
float normalizedX = coords.x / size.x;
float normalizedY = coords.y / size.y;
float wave = sin((normalizedX + normalizedY) * 6.0 + time) * 0.5 + 0.5;
return half4(0.0, 0.0, 0.0, wave);
}
""".trimIndent()
)
}
val customBlurRadius = remember(customShader) {
BlurRadiusSpec.shader(maxRadius = 24.dp) { customShader }
}
Modifier.blur {
val w = size.width.toPx()
val h = size.height.toPx()
customShader.setFloatUniform("size", w, h)
customShader.setFloatUniform("time", animatedTime)
blurRadiusSpec = customBlurRadius
},در اینجا سایهزن قطعه کوچکی مینویسیم که موج سینوسی قطری پویانماییشدهای را محاسبه میکند و آن شدت را در کانال آلفا خروجی میدهد.
سپس مشخصات سایهزن آن ماسک ۰ تا ۱ را مستقیماً به ماسک ۲۴ dp maxRadius نگاشت میکند.
اکنون قبلاز اینکه درباره محو کردن پسزمینه صحبت کنیم، بیایید تفاوت بین این دو را ببینیم.
اکنون در سمت راست، نوار برنامه بالایی را داریم که در آن محو کردن لایه را اعمال کردهایم.
یعنی خود ترکیبشدنی را محو میکند.
همانطور که میبینید، عنوان و نمادهای کنش تار هستند، اما مؤلفه گپ پشت آن اصلاً تار نیست.
در سمت راست، محو کردن پسزمینه را داریم.
در اینجا فقط پیامهای گپ که در پشت نوار بالا پیمایش میشوند نمونهبرداری و محو میشوند.
اما نوشتار و نمادها کاملاً واضح و خوانا هستند.
از Android 37.2، میتوانید بااستفاده از میانای برنامهسازی کاربردی جدیدی بهنام RenderNode.setBackdropRenderEffect، پسزمینه را محو کنید.
اما قبلاز اینکه به نحوه بهبود محو کردن بپردازیم، بیایید بفهمیم Android چگونه در پشت صحنه کار میکند تا مطمئن شویم محو کردن عملکرد خوبی دارد.
Android در پشت صحنه از RenderNodes استفاده میکند که محفظههای فهرست نمایش با شتاب سختافزاری هستند، یعنی میتوانند ازطریق شروع ضبط و پایان ضبط، مجموعهای از فرمانهای طراحی مختلف را فشرده و ذخیره کنند.
RenderNode اطلاعاتی مانند ترجمههای سهبعدی، چرخشها، آلفا، مقیاسها، و حدود موقعیت شما را نگه میدارد.
و Compose درواقع از RenderNode در پشت صحنه استفاده میکند.
بنابراین، برای استفاده از جلوههای پرداز پساویز در Compose، میتوانیم یک Modifier.Node سفارشی ایجاد کنیم تا بتوانیم از این اصلاحگر مجدداً استفاده کنیم.
fun Modifier.backdropBlur(spec: BlurRadiusSpec, shape: Shape, tint: Color): Modifier =
this then BackdropBlurElement(spec, shape, tint)
private class BackdropBlurNode(...) : Modifier.Node(), DrawModifierNode {
private var renderNode: RenderNode? = null
override fun ContentDrawScope.draw() {
val node = getOrCreateNode()
if (isDirty || sizeChanged) {
node.setPosition(0, 0, widthPx, heightPx)
node.setBackdropRenderEffect(spec.createRenderEffect(this))
}
drawIntoCanvas { it.nativeCanvas.drawRenderNode(node) }
drawContent()
}
}ما مرجعی به RenderNode نگه میداریم و RenderNode با جلوههایی که در تابع دیگری میخواهیم پیکربندی میشود.
برای مثال، در اینجا جلوه محو کردن را در blur ارسال میکنیم.
سپس، drawIntoCanvas را فراخوانی میکنیم، و drawRenderNode را در بوم بومیمان فراخوانی میکنیم.
و درنهایت، drawContent را فراخوانی میکنیم تا محتوای واقعی محونشده را نیز در بالا ترسیم کنیم.
برای استفاده از این اصلاحکنندهای که بهتازگی ایجاد کردهایم، سپس Modifier.backdropBlur را فراخوانی میکنیم و مشخصات شعاع محو کردن را ارسال میکنیم.
JetchatAppBar(
modifier = modifier
.backdropBlur(
tint = MaterialTheme.colorScheme
.surface.copy(alpha = 0.5f),
shape = RectangleShape,
spec = BlurRadiusSpec.uniform(5.dp)
)
)توجه داشته باشید که با این رویکرد، نیازی نیست چیزی روی لایه زیرین قرار دهیم.
RenderNode.setBackdropRenderEffect بهطور خودکار از محتوایی که زیر سطح نوار برنامه JetChat قرار دارد نمونهبرداری میکند.
و نتیجه این محو کردن پسزمینه لذتبخش است.
همچنین میتوانیم بهجای فقط یک محو تدریجی، مشخصه شعاع محو تدریجی تنظیم کنیم و محو عمودی نیز روی محتوا داشته باشیم.
JetchatAppBar(
modifier = modifier
.backdropBlur(
tint = MaterialTheme.colorScheme
.surface.copy(alpha = 0.5f),
shape = RectangleShape,
spec = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.5f, 32.dp),
BlurStop(1f, 0.dp),
),
)
)
)که با گرادیان عمودی، محتوای زیرین را با شعاعهای مختلف در بخشهای مختلف محفظه بهصورت محو نشان میدهد.
درحال کار روی اصلاحکننده محو کردن پسزمینه داخلی در Compose هستیم.
و این فقط نمونهای از فناوری زیربنایی در Android با Compose است.
این نمونهای که به شما نشان دادم فقط در سطح میانای برنامهسازی کاربردی ۳۷.۲ کار میکند، اما درحال کار روی یک نسخه سازگار با نسخههای قدیمیتر برای Compose نیز هستیم، پس منتظر باشید.
نمونه جالب دیگری از جلوههای پساویز RenderNode این است که میتوانیم آنها را بههم زنجیر کنیم تا واسطهای کاربری بسیار زیبایی ایجاد کنیم و جلوههای مختلفی را روی آن اعمال کنیم.
بنابراین برای زنجیرهای کردن جلوهها، این دو جلوه را ایجاد میکنیم.
val blur = RenderEffect.createBlurEffect(24f, 24f, Shader.TileMode.CLAMP)
// Boost saturation by 30% and apply blur together
val matrix = ColorMatrix().apply { setSaturation(1.3f) }
val vibrancy = RenderEffect.createColorFilterEffect(ColorMatrixColorFilter(matrix))
val vibrantBlur = RenderEffect.createChainEffect(vibrancy, blur)در این مثال، جلوه محو کردن و اشباع ۳۰٪ ایجاد میکنیم.
createChainEffect را فراخوانی میکنیم، ابتدا جلوه محو کردن اعمال میشود و سپس اشباع رنگ.
این نمونهای در JetChat است.
val agslShader = RuntimeShader(CHROMATIC_ABERRATION)
val noiseEffect = RenderEffect.createRuntimeShaderEffect(agslShader, "backdropInput")
val finalEffect = RenderEffect.createChainEffect(noiseEffect, vibrantBlur)
renderNode?.setBackdropRenderEffect(finalEffect)دکمه پرش به دکمه پرش به پایین، که میتوانیم با ترکیب چند جلوه مختلف با یکدیگر، آن را شبیه یک ذرهبین کوچک کنیم تا لبههای این ترکیبپذیر مانند ذرهبین گرد شود.
اما نهتنها میتوانید جلوههای بزرگنمایی را اعمال کنید، بلکه هر نوع سایهزنی را میتوان بهصورت زنجیرهای برای اعمال روی پسزمینه اعمال کرد.
در اینجا چند نمونه جالب از آنچه که هنگام ترکیب آنها با یکدیگر امکانپذیر است، آورده شده است.
اکنون جلوه سنتی شیشه مات امکانپذیر است.
اما میتوانیم جلوههای جالب دیگری مثل زیبابین هم ایجاد کنیم.
یا اگر بخواهیم این نماد Android را به نوعی عنصر واسط کاربر دگرگونشونده تبدیل کنیم که محتوای زیرین را تغییر میدهد، میتوانیم این جلوههای رندر پسزمینه را نیز اعمال کنیم تا بهنظر برسد که داریم ازطریق این ربات کوچک نگاه میکنیم.
و همچنین میتوانیم این کار را با «جلوههای پردازشی» پسزمینه انجام دهیم.
بنابراین، با این میانای برنامهسازی کاربردی جدید، نهتنها محو کردن پسزمینه و شیشه مات ما باز شده است، بلکه اکنون تنها محدودیت تخیل شما است.
میانای برنامهسازی کاربردی جدید دیگری که در پلاتفرم معرفی کردهایم SurfaceView.setBlurRegions است.
درحالحاضر میتوانید نماهای بافت را بااستفاده از Modifier.blur و RenderNode «جلوههای پساویز» محو کنید، اما محو کردن بخشهایی از SurfaceView دشوار است و بخشی از درخت RenderNode نمای برنامه نیست، زیرا درواقع سوراخی در پنجره ایجاد میکند.
بنابراین، Compose نمیتواند میانگیر پشتیبان SurfaceView را نمونهبرداری کند، که این کار را تا API 37.2 غیرممکن میکرد.
بنابراین، setBlurRegions مربوط به SurfaceView به برنامه اجازه میدهد مستقیماً مناطق محو را اعلام کند.
برای مثال، در این پخشکننده ویدیو در JetChat، وقتی کنترلها نشان داده و پنهان میشوند، محو کردن SurfaceView را به محو کردن سطح اضافه کردیم.
برای استفاده از این میانای برنامهسازی کاربردی جدید، میتوانید به setBlurRegions در روش SurfaceView دسترسی پیدا کنید.
val region = RoundedRectBlurRegion().apply {
bounds = RectF(left, top, right, bottom)
setCornerRadii(spec.cornerRadiusPx.coerceAtLeast(0f))
alpha = spec.alpha.coerceIn(0f, 1f)
blurRadius = spec.blurRadiusPx.coerceAtLeast(0f)
}
surfaceView.setBlurRegions(listOf(region))
surfaceView.setBlurRegions(emptyList()) // Clearاین گزینه فهرست نواحی مرزها، شعاع محوشدگی، آلفا، و فهرستی از شعاعهای گوشه را در آن میگیرد.
هنگام ارسال این مرزها، باید نسبت به SurfaceView روی صفحه باشند.
بنابراین، باید مختصات SurfaceView و ناحیهای را که میخواهید محو کنید ردگیری کنید.
یکی دیگر از ویژگیهای جدید Android 16 و نسخههای بالاتر RuntimeColorFilter است.
قبلاً با تصاویر فقط میتوانستید فیلتر رنگ ماتریس ثابت تنظیم کنید و جلوههایی مانند اشباع را اعمال کنید.
بااینحال، با Android 16 و بالاتر، اکنون میتوانید سایهزنی را ارسال کنید که رنگ ورودی، پیکسل ورودی برای آن محتوای خاص را میگیرد و رنگ جدیدی تولید میکند.
برای مثال، اگر بخواهیم فیلتر سایبرپانک را روی عکس اعمال کنیم، میتوانیم سایهزن کوچکی تعریف کنیم که بهعنوان ورودی در RuntimeColorFilter استفاده میشود.
@Language("AGSL")
private const val CYBERPUNK_SHADER = """
vec4 main(half4 in_color) {
float lum = dot(in_color.rgb, vec3(0.299, 0.587, 0.114));
vec3 shadow = vec3(0.06, 0.04, 0.25);
vec3 mid = vec3(0.92, 0.12, 0.58);
vec3 high = vec3(0.18, 0.94, 0.98);
vec3 col = lum < 0.5 ? mix(shadow, mid, lum * 2.0) : mix(mid, high, (lum - 0.5) * 2.0);
return vec4(mix(in_color.rgb, col, 0.7), in_color.a);
}
"""این سایهزن یک تم سایبرپانک ایجاد میکند، اما شما میتوانید هر جلوه متفاوت دیگری را که میخواهید ایجاد کنید.
بنابراین، برای استفاده از خود سایهزن، RuntimeColorFilter را اعمال میکنیم و آن را به فیلتر رنگ Compose تبدیل میکنیم.
AsyncImage(
model = imageUri,
contentDescription = filter.displayName,
colorFilter = RuntimeColorFilter(CYBERPUNK_SHADER)
.asComposeColorFilter(),
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)همانطور که در این مثال میبینید، اکنون میتوانیم جلوههای پویاتری نسبتبه قبل ایجاد کنیم بدون اینکه نیاز باشد عمیقتر وارد OpenGL، راهاندازیهای SurfaceView، یا هر چیز دیگری از این قبیل شویم.
و این را میتوان برای هر ترکیبپذیری، نه فقط تصاویر، اعمال کرد.
متشکریم.
بنابراین، جلوه بسیار خوبی ساختهایم، اما بیایید مطمئن شویم که برنامهمان همچنان سریع و روان است.
وقتی در مکالمه پیمایش میکنیم، همه جلوههای ما باهم اجرا میشوند.
بیایید با اندازهگیری این طومار شروع کنیم.
@Test
fun scrollFling() = measureScroll(swipeSteps = 5)
private fun measureScroll(swipeSteps: Int) = rule.measureRepeated(
packageName = TARGET_PACKAGE,
metrics = defaultComposeScrollingMetrics(),
compilationMode = CompilationMode.Full(),
iterations = 5,
setupBlock = {
killProcess()
}
//...ما از یک محک ماکرو استفاده میکنیم که برنامهای را در فرایندی جدید راهاندازی میکند و پیمایش در مکالمه را اجرا میکند.
سپس، ردگیری را جستجو میکنیم تا فریمهایی را که به مهلت مقرر خود نرسیدهاند بشماریم.
select count(*) as late_frames
from actual_frame_timeline_slice
where upid = (select upid from process where name = 'com.example.compose.jetchat.perf')
and jank_type like '%App Deadline Missed%'این به ما شش فریم دیرکرد میدهد.
بیایید یکی از قابهای آخر را باز کنیم.
بنابراین، درست قبلاز این قاب، Compose پیام بعدی را قبلاز اینکه به بالا پیمایش شود و در نما قرار گیرد، پیشواکشی میکند.
پیشواکشی باید در فاصله کوتاه بین قابها قرار گیرد، اما آمادهسازی این پیام همچنین تصویر آن را مستقیماً در رشته اصلی رمزگشایی میکند و بدون بازدهی طولانی اجرا میشود.
چون رشته اصلی درحال رمزگشایی این تصویر است، فریم جدیدی نمیتواند شروع شود.
سپس، وقتی آن پیام واقعاً در نما پیمایش میشود، RenderThread باید حافظه اختصاص دهد و همه آن پیکسلها را در GPU بارگذاری کند، بنابراین همان تصویر دقیقاً فریم دوم را بهتأخیر میاندازد.
اکنون میبینیم که زمان ما کجا صرف شده است، اما آیا واقعاً برای ترسیم این قاب به این همه کار نیاز داریم؟
و چطور میتوانیم تشخیص دهیم؟
ردیابی به ما میگوید.
هرگاه نگاشت بیتی بزرگی در ناحیه بسیار کوچکتری روی صفحهنمایش ترسیم شود، Android نشانگری را مستقیماً در رد ثبت میکند.
دو عدد را به پیکسل نشان میدهد: ابعاد کامل نقشه بیتی، و منطقهای که واقعاً در صفحه پوشش میدهد.
آن را بر تراکم صفحهنمایش تقسیم کنید تا مقدار DP را بهدست آورید.
سپس، کد خود را با آن مقدار DP جستجو میکنیم تا آن تصویر را پیدا کنیم.
این چیزی است که پیدا میکنیم.
تصویر با painterResource بار میشود که نمیداند اندازه محتوی چقدر است، بنابراین کل تصویر را با وضوح کامل رمزگشایی میکند.
Image(
painter = painterResource(it),
contentScale = ContentScale.Fit,
modifier = Modifier.size(160.dp),
contentDescription = stringResource(id = R.string.attached_image),
)بنابراین، آن را با AsyncImage از سیمپیچ جایگزین میکنیم.
AsyncImage(
model = it,
contentScale = ContentScale.Fit,
modifier = Modifier.size(160.dp),
contentDescription = stringResource(id = R.string.attached_image),
)این قالب ابتدا ظرف را اندازهگیری میکند، بنابراین فقط پیکسل موردنیاز را رمزگشایی میکند و این کار را در رشته دیگری انجام میدهد.
با اجرای مجدد همان محک، رمزگشایی تصویر در رشته اصلی به ۰ کاهش یافت و قاب دیرهنگام به ۲ کاهش یافت.
ما این دو فریم را در اینجا اشکالزدایی نخواهیم کرد، اما نکته ساده است: سعی کنید پیکسلی را که هرگز به فریم نمیرسد محاسبه نکنید.
بنابراین، رمزگشایی تصویر بهاندازهای بزرگ بود که باعث تأخیر در فریمها شود، اما هر هزینهای بهعنوان فریم حذفشده نشان داده نمیشود و هنوز هزینههای کوچکتری وجود دارد که میتوانیم هنگام نوشتن کد از آنها اجتناب کنیم.
بنابراین، بیایید دو مورد از آنها را ببینیم.
هرزمان از RuntimeShader استفاده میکنیم، باید آن را یکبار ایجاد کنیم زیرا اگرچه فقط کامپایل و ذخیره موقت میشود، اما همچنان هربار که آن را ایجاد میکنیم، رشته منبع خود را تجزیه میکند.
و هرگاه جلوهای هر قاب را پویانمایی کند، میتوانیم آن را در graphicsLayer بپیچیم تا فقط عنصر پویانمایی دوباره ضبط شود و واسط کاربر اطراف آن دوباره ترسیم نشود.
دکمه «پرش به پایین» هروقت پیمایش کنیم نشان داده میشود.
این از دو RuntimeShader برای نشان دادن آن جلوه استفاده میکند، و هربار که دکمه ظاهر میشود، ما هر دو، هر دو این سایهزنها را از رشته منبع آن، آنها بازآفرینی میکنیم.
val lensShader = RuntimeShader(MAGNIFIER_LENS_SHADER).apply {
setFloatUniform("size", size.width, size.height)
setFloatUniform("zoom", zoom)
// …
val lensEffect = RenderEffect.createRuntimeShaderEffect(lensShader, "content")
// …
val glassShader = RuntimeShader(GLASS_FINISH_SHADER).apply {تجزیه آن دو سایهزن و ساختن جلوه هربار که دکمه نشان داده میشود صدها میکروثانیه در رشته اصلی طول میکشد.
درعوض، سایهزن را یکبار برای کل فرایند ایجاد میکنیم، یا میتوانیم آن را بهخاطر بسپاریم و سپس یونیفرم آن را بهروز کنیم.
val lensShader = MagnifierShaders.lens.apply {
setFloatUniform("size", size.width, size.height)
setFloatUniform("zoom", zoom)
// …
private object MagnifierShaders {
val lens: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(MAGNIFIER_LENS_SHADER) }
val glassFinish: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(GLASS_FINISH_SHADER) }با این تغییر، ساختن آن جلوه تقریباً ۴۰ برابر سریعتر شده است و هزینه ظاهر شدن دکمه ۲۰ برابر کاهش یافته است.
در درخشش گرادیان مش که قبلاً دیدیم، درخشش هر قاب را در drawBehind پویانمایی میکند.
Box(
modifier = Modifier
.matchParentSize()
.drawBehind {
// Behind the card, scaled past its bounds.
translate(top = -58.dp.toPx()) {
scale(scaleX = 1.38f, scaleY = 2.85f) {
with(glowMeshPainter) {
draw(size, alpha = glowAlpha)
}بنابراین خواندن وضعیت در فاز ترسیم، ترکیببندی و چیدمان را حفظ میکند، اما فهرست نمایش نمیتواند یک فرمان ترسیم را در محل بهروز کند.
بنابراین Compose درخت را بالا میرود تا نزدیکترین گره با لایه گرافیکی را پیدا کند، که در اینجا، طرح کلی قرمز است، که کل صفحه است، و کل آن لایه را در هر فریم دوباره ضبط میکند.
با قرار دادن graphicsLayer درست روی چارگوش، پیمایش در حاشیه سبز متوقف میشود، بنابراین فقط چارگوش درخشان دوباره ضبط میشود و برش ترسیم رشته اصلی دو دهم میلیثانیه کاهش مییابد.
Box(
modifier = Modifier
.matchParentSize()
.graphicsLayer()
.drawBehind {
// Behind the card, scaled past its bounds.
translate(top = -58.dp.toPx()) {
scale(scaleX = 1.38f, scaleY = 2.85f) {
with(glowMeshPainter) {
draw(size, alpha = glowAlpha)
}با Perfetto، تجزیهوتحلیل آنچه در ردگیری اشتباه پیش میرود میتواند دشوار باشد، بنابراین میتوانید از مهارت تجزیهوتحلیل ردگیری برای کمک به جمعآوری برخی اطلاعات آماری استفاده کنید.
و اگر برای نوشتن کد Compose با عملکرد بالا به کمک بیشتری نیاز دارید، حتماً Android CLI را نصب کنید و برای شروع از مهارت عملکرد Compose استفاده کنید.
android skills add perfetto-trace-analysis
android skills add jetpack-compose-performanceval gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
}
}val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
}
}val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
// ....
setVertex(1, 1, Offset(0.2f, 0.4f) + Offset(animatedOffset, animatedOffset), coral)
// ....
setVertex(3, 0, Offset(0.0f, 1.0f), sunshine)
// ....
}
}
Box(
modifier = modifier.padding(32.dp)
.safeContentPadding()
.aspectRatio(16 / 9f)
.fillMaxWidth()
.paint(gradientPainter)
)MeshGradientPainter(
rows = 2,
columns = 3,
hasBicubicColor = true,
) {
// Row 0 (top edge, y = 0.0f)
setVertex(0, 0, Offset(0.0000f, 0.0000f), Color(0xFFFF6B6B))
setVertex(0, 1, Offset(0.3300f, 0.0000f), Color(0xFFFFD6D6))
setVertex(0, 2, Offset(0.6700f, 0.0000f), Color(0xFFE8C3FF))
setVertex(0, 3, Offset(1.0000f, 0.0000f), Color(0xFFE396FF))
// ... remaining vertices for Rows 0, 1, and 2
}val transition = rememberInfiniteTransition(label = "userInputGlowMesh")
val phase1 by transition.animateFloat(
initialValue = 0f,
targetValue = (2 * PI).toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 2800, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "glowPhase1",
)
val phase2 by transition.animateFloat(
initialValue = 0f,
targetValue = (2 * PI).toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 4200, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "glowPhase2",
)MeshGradientPainter(
rows = 6,
columns = 5,
hasBicubicColor = true,
) {
for (r in 0..6) {
for (c in 0..5) {
val offset = if (r in 1..5 && c in 1..4) {
val ampX = when (r) {
1, 2 -> 0.048f
3 -> 0.042f
4 -> 0.028f
else -> 0.036f
}
val ampY = when (r) {
1, 2 -> 0.028f
3 -> 0.020f
4 -> 0.022f
else -> 0.016f
}
val d = orbit(c, r, ampX, ampY)
Offset(
x = (baseU + d.x).coerceIn(0.04f, 0.96f),
y = (baseV + d.y).coerceIn(0.05f, 0.95f),
)
} else {
Offset(baseU, baseV)
}
setVertex(r, c, offset, gridColors[r][c])
}
}
}val lines = remember {
path.asAndroidPath().flatten(0.5f)
}
//... inside draw scope
lines.forEach { line ->
//...
drawLine(
brush = Brush.linearGradient(...),
start = Offset(line.start.x,
line.start.y),
end = Offset(line.end.x, line.end.y),
strokeWidth = 30f,
)
//...
}LineShaders.VERTEX
uniform float2 uStart;
uniform float2 uEnd;
uniform float uHalfWidth;
uniform float uTime;
Varyings main(const Attributes a) {
Varyings v;
float2 dir = uEnd - uStart;
float2 normal = normalize(float2(-dir.y, dir.x));
float along = a.corner.x * length(dir);
float pulse = (a.corner.x - uTime) / 0.18;
float radius = uHalfWidth * (1.0 + 0.6 * exp(-pulse * pulse));
float across = a.corner.y * (radius + 1.0);
v.position = uStart + dir * a.corner.x + normal * across;
v.stroke = float3(along, across, radius);
return v;
}LineShaders.FRAGMENT
uniform float uLength;
layout(color) uniform float4 uStartColor;
layout(color) uniform float4 uEndColor;
float2 main(const Varyings v, out float4 color) {
float distance = abs(v.stroke.y);
float coverage = 1.0 - smoothstep(
v.stroke.z - 0.5, v.stroke.z + 0.5, distance);
float side = distance / v.stroke.z;
float4 ink = mix(uStartColor, uEndColor,
clamp(v.stroke.x / uLength + 0.4 * side * side, 0.0, 1.0));
color = ink * coverage;
return v.position;
}val spec = remember {
MeshSpecification.make(
arrayOf(Attribute(TYPE_FLOAT2, 0, "corner")),
vertexStride = 8,
arrayOf(Varying(TYPE_FLOAT3, "stroke")),
LineShaders.VERTEX, LineShaders.FRAGMENT)
}fun lineCorners(segments: Int): ByteBuffer {
val buffer = ByteBuffer.allocateDirect((segments + 1) * 2 * 8)
.order(ByteOrder.nativeOrder())
for (i in 0..segments) {
val t = i / segments.toFloat()
buffer.putFloat(t).putFloat(-1f) // one side
buffer.putFloat(t).putFloat(1f) // the other side
}
return buffer.rewind() as ByteBuffer
}val mesh = Mesh(
spec,
mode = Mesh.TRIANGLE_STRIP,
vertexBuffer = corners,
vertexCount = (segments + 1) * 2,
bounds = RectF(0f, 0f, size.width, size.height)
)Box(Modifier.fillMaxSize().drawWithCache {
val mesh = Mesh(...)
onDrawBehind {
mesh.setFloatUniform("uTime", time.value)
drawIntoCanvas {
it.nativeCanvas.drawMesh(mesh,android.graphics.BlendMode.DST, paint)
}
}
})<!-- AndroidManifest.xml -->
<activity
android:name=".NavActivity"
android:colorMode="wideColorGamut"
android:exported="true">
</activity>// Dynamic toggle in code
DisposableEffect(window) {
window?.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
onDispose {
window?.colorMode = ActivityInfo.COLOR_MODE_DEFAULT
}
}// 1. Display P3 Color (Wider gamut: greens & reds are more saturated) colorMode=wideColorGamut
val VividP3Red = Color(
red = 1.0f,
green = 0.05f,
blue = 0.05f,
alpha = 1.0f,
colorSpace = ColorSpaces.DisplayP3
)
// 2. Extended sRGB (Enables HDR values > 1.0f) use colorMode=hdr
val HdrNeonCyan = Color(
red = 0.0f,
green = 1.8f, // Peak brightness > 1.0 in extended gamut
blue = 2.0f,
alpha = 1.0f,
colorSpace = ColorSpaces.ExtendedSrgb
)
// 3. BT.2020 HLG (Broadcast HDR standard, supported API 34+) - use colorMode=hdr
val HdrSunsetOrange = Color(
red = 1.0f,
green = 0.45f,
blue = 0.1f,
alpha = 1.0f,
colorSpace = ColorSpaces.Bt2020Hlg
)fun cookiePolygon(): RoundedPolygon {
val vertices = FloatArray(32)
for (i in 0 until 16) {
val radius = if (i % 2 == 0) 1f else 0.84f
// …
}
return RoundedPolygon(
vertices,
rounding = CornerRounding(0.12f)
)
}class RoundedPolygonShape(
polygon: RoundedPolygonShape
) : Shape {
// …
override fun createOutline(size: Size, layoutDirection: LayoutDirection,
density: Density,
): Outline {
path = polygon.toPath().asComposePath()
// …
matrix.scale(size.width / maxDimension, size.height / maxDimension)
matrix.translate(-bounds.left, -bounds.top)
// …
return Outline.Generic(path)
}
}private val AvatarShape = PolygonShape {
val vertices = List(16) { i ->
val radius = size.minDimension / 2f * if (i % 2 == 0) 1f else 0.86f
val angle = i * PI.toFloat() / 8f
size.center + Offset(cos(angle), sin(angle)) * radius
}
polygon(vertices, rounding = CornerRounding(radius = 8.dp))
}
// …
Modifier.clip(AvatarShape)val recordShape = remember {
MorphPolygonShape(
start = PolygonShape.circle(),
end = PolygonShape.star(
numPoints = 9,
innerRadiusRatio = 0.8f,
outerRounding = CornerRounding(percent = 50),
),
) { shapeProgress.value }
}Modifier
.blur(
radiusX = 10.dp,
radiusY = 10.dp,
edgeTreatment = BlurredEdgeTreatment(
shape = RoundedCornerShape(8.dp),
),
)Modifier.blur {
blurRadiusSpec = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.5f, 0.dp),
BlurStop(1.0f, 32.dp),
)
)
edgeTreatment = BlurredEdgeTreatment.Unbounded
}BlurRadiusSpec.horizontalGradient(
startRadius = 0.dp,
endRadius = sliderValue.dp,
)BlurRadiusSpec.radialGradient(
center = center,
fallOffRadius = 300.dp,
startRadius = 0.dp,
endRadius = 24.dp,
)val customShader = remember {
RuntimeShader(
"""
uniform float2 size;
uniform float time;
half4 main(float2 coords) {
// Diagonal wave mask
float normalizedX = coords.x / size.x;
float normalizedY = coords.y / size.y;
float wave = sin((normalizedX + normalizedY) * 6.0 + time) * 0.5 + 0.5;
return half4(0.0, 0.0, 0.0, wave);
}
""".trimIndent()
)
}
val customBlurRadius = remember(customShader) {
BlurRadiusSpec.shader(maxRadius = 24.dp) { customShader }
}
Modifier.blur {
val w = size.width.toPx()
val h = size.height.toPx()
customShader.setFloatUniform("size", w, h)
customShader.setFloatUniform("time", animatedTime)
blurRadiusSpec = customBlurRadius
},fun Modifier.backdropBlur(spec: BlurRadiusSpec, shape: Shape, tint: Color): Modifier =
this then BackdropBlurElement(spec, shape, tint)
private class BackdropBlurNode(...) : Modifier.Node(), DrawModifierNode {
private var renderNode: RenderNode? = null
override fun ContentDrawScope.draw() {
val node = getOrCreateNode()
if (isDirty || sizeChanged) {
node.setPosition(0, 0, widthPx, heightPx)
node.setBackdropRenderEffect(spec.createRenderEffect(this))
}
drawIntoCanvas { it.nativeCanvas.drawRenderNode(node) }
drawContent()
}
}JetchatAppBar(
modifier = modifier
.backdropBlur(
tint = MaterialTheme.colorScheme
.surface.copy(alpha = 0.5f),
shape = RectangleShape,
spec = BlurRadiusSpec.uniform(5.dp)
)
)JetchatAppBar(
modifier = modifier
.backdropBlur(
tint = MaterialTheme.colorScheme
.surface.copy(alpha = 0.5f),
shape = RectangleShape,
spec = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.5f, 32.dp),
BlurStop(1f, 0.dp),
),
)
)
)val blur = RenderEffect.createBlurEffect(24f, 24f, Shader.TileMode.CLAMP)
// Boost saturation by 30% and apply blur together
val matrix = ColorMatrix().apply { setSaturation(1.3f) }
val vibrancy = RenderEffect.createColorFilterEffect(ColorMatrixColorFilter(matrix))
val vibrantBlur = RenderEffect.createChainEffect(vibrancy, blur)val agslShader = RuntimeShader(CHROMATIC_ABERRATION)
val noiseEffect = RenderEffect.createRuntimeShaderEffect(agslShader, "backdropInput")
val finalEffect = RenderEffect.createChainEffect(noiseEffect, vibrantBlur)
renderNode?.setBackdropRenderEffect(finalEffect)val region = RoundedRectBlurRegion().apply {
bounds = RectF(left, top, right, bottom)
setCornerRadii(spec.cornerRadiusPx.coerceAtLeast(0f))
alpha = spec.alpha.coerceIn(0f, 1f)
blurRadius = spec.blurRadiusPx.coerceAtLeast(0f)
}
surfaceView.setBlurRegions(listOf(region))
surfaceView.setBlurRegions(emptyList()) // Clear@Language("AGSL")
private const val CYBERPUNK_SHADER = """
vec4 main(half4 in_color) {
float lum = dot(in_color.rgb, vec3(0.299, 0.587, 0.114));
vec3 shadow = vec3(0.06, 0.04, 0.25);
vec3 mid = vec3(0.92, 0.12, 0.58);
vec3 high = vec3(0.18, 0.94, 0.98);
vec3 col = lum < 0.5 ? mix(shadow, mid, lum * 2.0) : mix(mid, high, (lum - 0.5) * 2.0);
return vec4(mix(in_color.rgb, col, 0.7), in_color.a);
}
"""AsyncImage(
model = imageUri,
contentDescription = filter.displayName,
colorFilter = RuntimeColorFilter(CYBERPUNK_SHADER)
.asComposeColorFilter(),
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)@Test
fun scrollFling() = measureScroll(swipeSteps = 5)
private fun measureScroll(swipeSteps: Int) = rule.measureRepeated(
packageName = TARGET_PACKAGE,
metrics = defaultComposeScrollingMetrics(),
compilationMode = CompilationMode.Full(),
iterations = 5,
setupBlock = {
killProcess()
}
//...select count(*) as late_frames
from actual_frame_timeline_slice
where upid = (select upid from process where name = 'com.example.compose.jetchat.perf')
and jank_type like '%App Deadline Missed%'Image(
painter = painterResource(it),
contentScale = ContentScale.Fit,
modifier = Modifier.size(160.dp),
contentDescription = stringResource(id = R.string.attached_image),
)AsyncImage(
model = it,
contentScale = ContentScale.Fit,
modifier = Modifier.size(160.dp),
contentDescription = stringResource(id = R.string.attached_image),
)val lensShader = RuntimeShader(MAGNIFIER_LENS_SHADER).apply {
setFloatUniform("size", size.width, size.height)
setFloatUniform("zoom", zoom)
// …
val lensEffect = RenderEffect.createRuntimeShaderEffect(lensShader, "content")
// …
val glassShader = RuntimeShader(GLASS_FINISH_SHADER).apply {val lensShader = MagnifierShaders.lens.apply {
setFloatUniform("size", size.width, size.height)
setFloatUniform("zoom", zoom)
// …
private object MagnifierShaders {
val lens: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(MAGNIFIER_LENS_SHADER) }
val glassFinish: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(GLASS_FINISH_SHADER) }Box(
modifier = Modifier
.matchParentSize()
.drawBehind {
// Behind the card, scaled past its bounds.
translate(top = -58.dp.toPx()) {
scale(scaleX = 1.38f, scaleY = 2.85f) {
with(glowMeshPainter) {
draw(size, alpha = glowAlpha)
}Box(
modifier = Modifier
.matchParentSize()
.graphicsLayer()
.drawBehind {
// Behind the card, scaled past its bounds.
translate(top = -58.dp.toPx()) {
scale(scaleX = 1.38f, scaleY = 2.85f) {
with(glowMeshPainter) {
draw(size, alpha = glowAlpha)
}android skills add perfetto-trace-analysis
android skills add jetpack-compose-performance