علاوهبر Canvas قابل ترکیب، «نوشتن» چندین گرافیک مفید دارد
Modifiers که به طراحی محتوای سفارشی کمک میکند. این اصلاحکنندهها مفید هستند
زیرا میتوانند روی هر عنصر ترکیبی اعمال شوند.
اصلاحگرهای طراحی
همه فرمانهای طراحی با اصلاحکننده طراحی در «نوشتن» انجام میشود. سه اصلاحگر اصلی برای طراحی در «نوشتن» وجود دارد:
اصلاحگر پایه برای طراحی drawWithContent است، که در آن میتوانید
ترتیب طراحی «ترکیبی» خود و دستورات طراحی صادرشده در داخل
اصلاحگر را تعیین کنید. drawBehind یک بستهبندی مناسب برای drawWithContent است که
ترتیب طراحی آن روی پشت محتوای عنصر ترکیبی تنظیم شده است. drawWithCacheیا onDrawBehind یا onDrawWithContent را در داخل خود فرا میخواند - و سازوکاری برای ذخیره کردن اشیای ایجادشده در آنها ارائه میدهد.
Modifier.drawWithContent: انتخاب ترتیب قرعهکشی
Modifier.drawWithContent به شما امکان میدهد
عملیات DrawScope را قبل یا بعداز محتوای
ترکیبی اجرا کنید. حتماً drawContent را فراخوانی کنید تا محتوای واقعی
ترکیبی را ارائه کند. با این اصلاحگر، میتوانید ترتیب عملیات را تعیین کنید، اینکه
میخواهید محتوایتان قبلاز عملیات رسم سفارشیتان رسم شود یا بعداز آن.
برای مثال، اگر میخواهید یک گرادیان شعاعی را روی محتوای خود رندر کنید تا یک جلوه سوراخ کلید چراغ قوه در رابط کاربری ایجاد کنید، میتوانید کارهای زیر را انجام دهید:
var pointerOffset by remember { mutableStateOf(Offset(0f, 0f)) } Column( modifier = Modifier .fillMaxSize() .pointerInput("dragging") { detectDragGestures { change, dragAmount -> pointerOffset += dragAmount } } .onSizeChanged { pointerOffset = Offset(it.width / 2f, it.height / 2f) } .drawWithContent { drawContent() // draws a fully black area with a small keyhole at pointerOffset that’ll show part of the UI. drawRect( Brush.radialGradient( listOf(Color.Transparent, Color.Black), center = pointerOffset, radius = 100.dp.toPx(), ) ) } ) { // Your composables here }
Modifier.drawBehind: طراحی در پشت عنصر ترکیبی
Modifier.drawBehind به شما امکان میدهد
DrawScope عملیات را در پس محتوای ترکیبی که روی صفحه کشیده میشود انجام دهید. اگر به پیادهسازی Canvas نگاهی بیندازید، ممکن است متوجه شوید که این فقط یک بستهبندی راحت برای Modifier.drawBehind است.
برای کشیدن مستطیل گردشده در پشت Text:
Text( "Hello Compose!", modifier = Modifier .drawBehind { drawRoundRect( Color(0xFFBBAAEE), cornerRadius = CornerRadius(10.dp.toPx()) ) } .padding(4.dp) )
که نتیجه زیر را تولید میکند:
Modifier.drawWithCache: رسم و ذخیره کردن اشیای رسم
Modifier.drawWithCache اشیایی را که
در داخل آن ایجاد شدهاند در حافظه نهان نگه میدارد. تا زمانی که اندازه
ناحیه طراحی یکسان باشد، یا هریک از وضعیتهای خواندهشده
تغییر نکرده باشد، اشیا در حافظه نهان ذخیره میشوند. این اصلاحگر برای بهبود عملکرد فراخوانیهای طراحی مفید است زیرا
از نیاز به تخصیص مجدد اشیایی (مثل: Brush, Shader, Path و غیره)
که در طراحی ایجاد میشوند جلوگیری میکند.
یا میتوانید اشیا را بااستفاده از remember، خارج از
اصلاحکننده، ذخیره کنید. بااینحال، این کار همیشه ممکن نیست زیرا همیشه به قطعه موسیقی دسترسی ندارید. اگر از اشیا فقط برای رسم کردن استفاده میشود، استفاده از drawWithCache میتواند عملکرد بهتری داشته باشد.
برای مثال، اگر Brush را برای رسم گرادیان در پشت Text ایجاد کنید، بااستفاده از
drawWithCache، هدف Brush را تا زمانیکه اندازه ناحیه رسم تغییر کند
ذخیره میکند:
Text( "Hello Compose!", modifier = Modifier .drawWithCache { val brush = Brush.linearGradient( listOf( Color(0xFF9E82F0), Color(0xFF42A5F5) ) ) onDrawBehind { drawRoundRect( brush, cornerRadius = CornerRadius(10.dp.toPx()) ) } } )
اصلاحکنندههای گرافیک
Modifier.graphicsLayer: اعمال تبدیلها روی عناصر ترکیبی
Modifier.graphicsLayer
یک اصلاحکننده است که باعث میشود محتوای عنصر ترکیبی در لایه طراحی کشیده شود. لایه
چند عملکرد مختلف ارائه میدهد، ازجمله:
- جداسازی دستورالعملهای طراحی آن (مشابه
RenderNode). دستورالعملهای طراحی که بهعنوان بخشی از لایه ضبط شدهاند میتوانند بهطور کارآمد توسط خط لوله رندرینگ بدون اجرای مجدد کد برنامه مجدداً صادر شوند. - تبدیلهایی که روی همه دستورالعملهای طراحی موجود در یک لایه اعمال میشود.
- شطرنجیسازی برای قابلیتهای ترکیببندی. وقتی لایهای شطرنجی میشود، دستورالعملهای طراحی آن اجرا میشود و برونداد در بافر خارج از صفحه ضبط میشود. ترکیب کردن چنین بافری برای قابهای بعدی سریعتر از اجرای دستورالعملهای جداگانه است، اما وقتی تبدیلهایی مثل مقیاسبندی یا چرخش اعمال میشود، این بافر بهعنوان یک بیتمپ عمل میکند.
تبدیلها
Modifier.graphicsLayer برای دستورالعملهای طراحی خود ایزوله ارائه میدهد؛
برای مثال، بااستفاده از
Modifier.graphicsLayer میتوان تبدیلهای مختلفی را اعمال کرد. اینها را میتوان بدون نیاز به اجرای مجدد لامبدای طراحی، پویانمایی یا اصلاح کرد.
Modifier.graphicsLayer اندازه یا جای چیدمان شما را تغییر نمیدهد، زیرا فقط بر مرحله طراحی تأثیر میگذارد. این یعنی عنصر ترکیبی شما
اگر در خارج از محدوده چیدمان خود رسم شود، ممکن است با عناصر دیگر همپوشانی داشته باشد.
تبدیلهای زیر را میتوان با این اصلاحکننده اعمال کرد:
ترازو - افزایش اندازه
scaleX و scaleY محتوا را بهترتیب در جهت افقی یا عمودی بزرگ یا کوچک میکند. مقدار 1.0f نشاندهنده عدم تغییر در مقیاس است، مقدار
0.5f به معنای نصف ابعاد است.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.scaleX = 1.2f this.scaleY = 0.8f } )
ترجمه
translationX و translationY را میتوان با graphicsLayer تغییر داد،
translationX عنصر ترکیبی را به چپ یا راست میبرد. translationY عنصر
ترکیبی را بهبالا یا پایین میبرد.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.translationX = 100.dp.toPx() this.translationY = 10.dp.toPx() } )
چرخش
rotationX را برای چرخش افقی، rotationY را برای چرخش عمودی، و
rotationZ را برای چرخش در محور Z (چرخش استاندارد) تنظیم کنید. این مقدار برحسب درجه (۰ تا ۳۶۰) مشخص میشود.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.rotationX = 90f this.rotationY = 275f this.rotationZ = 180f } )
مبدأ
میتوانید transformOrigin را مشخص کنید. سپس بهعنوان نقطهای که
تبدیلها از آنجا انجام میشود استفاده میشود. تاکنون همه مثالها از
TransformOrigin.Center استفاده کردهاند که در (0.5f, 0.5f) قرار دارد. اگر مبدأ را در
(0f, 0f) مشخص کنید، تبدیلها از گوشه بالا سمت راست
عنصر ترکیبپذیر شروع میشود.
اگر مبدأ را با تبدیل rotationZ تغییر دهید، میتوانید ببینید که
عنصر حول گوشه بالا سمت چپ عنصر ترکیبشدنی میچرخد:
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.transformOrigin = TransformOrigin(0f, 0f) this.rotationX = 90f this.rotationY = 275f this.rotationZ = 180f } )
برش و شکل
شکل مشخص میکند که محتوا هنگام clip = true به کدام خطوط کلی چسبانده شود. در این مثال، دو چارگوش را تنظیم میکنیم تا دو کلیپ متفاوت داشته باشند - یکی بااستفاده از متغیر کلیپ graphicsLayer و دیگری بااستفاده از بستهبندی مناسب Modifier.clip.
Column(modifier = Modifier.padding(16.dp)) { Box( modifier = Modifier .size(200.dp) .graphicsLayer { clip = true shape = CircleShape } .background(Color(0xFFF06292)) ) { Text( "Hello Compose", style = TextStyle(color = Color.Black, fontSize = 46.sp), modifier = Modifier.align(Alignment.Center) ) } Box( modifier = Modifier .size(200.dp) .clip(CircleShape) .background(Color(0xFF4DB6AC)) ) }
محتوای چارگوش اول (نوشتار «سلام نوشتن») به شکل دایره برش داده میشود:
اگر سپس translationY را به دایره صورتی بالا اعمال کنید، میبینید که حدود
«ترکیبشونده» همچنان یکسان است، اما دایره در زیر دایره پایین (و خارج از حدود آن) رسم میشود.
برای چسباندن عنصر ترکیبی به ناحیهای که در آن رسم شده است، میتوانید
Modifier.clip(RectangleShape) دیگری را در ابتدای زنجیره اصلاحگر اضافه کنید. محتوا
سپس در محدوده اصلی باقی میماند.
Column(modifier = Modifier.padding(16.dp)) { Box( modifier = Modifier .clip(RectangleShape) .size(200.dp) .border(2.dp, Color.Black) .graphicsLayer { clip = true shape = CircleShape translationY = 50.dp.toPx() } .background(Color(0xFFF06292)) ) { Text( "Hello Compose", style = TextStyle(color = Color.Black, fontSize = 46.sp), modifier = Modifier.align(Alignment.Center) ) } Box( modifier = Modifier .size(200.dp) .clip(RoundedCornerShape(500.dp)) .background(Color(0xFF4DB6AC)) ) }
آلفا
از Modifier.graphicsLayer میتوان برای تنظیم alpha (کدری) برای کل لایه استفاده کرد. 1.0f کاملاً مات است و 0.0f نامرئی است.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "clock", modifier = Modifier .graphicsLayer { this.alpha = 0.5f } )
راهبرد ترکیب
کار با آلفا و شفافیت همیشه به سادگی تغییر یک مقدار آلفا نیست. علاوهبر تغییر آلفا، گزینه تنظیم CompositingStrategy روی graphicsLayer نیز وجود دارد. CompositingStrategy تعیین میکند که محتوای
عنصر ترکیبی چگونه با محتوای دیگری که
قبلاً روی صفحه کشیده شده است ترکیب شود (کنار هم قرار گیرد).
استراتژیهای مختلف عبارتاند از:
خودکار (پیشفرض)
استراتژی ترکیببندی توسط بقیه graphicsLayer
پارامترها تعیین میشود. اگر آلفا کمتر از
1.0f باشد یا RenderEffect تنظیم شده باشد، لایه را در میانگیری خارج از صفحه پرداز میکند. هرگاه آلفا کمتر از ۱f باشد، لایه ترکیبی بهطور خودکار برای پرداز محتوا ایجاد میشود و سپس این بافر خارج از صفحه با آلفای مربوطه در مقصد رسم میشود. تنظیم
RenderEffect یا پیمایش اضافی همیشه محتوا را در
بافری خارج از صفحه صرفنظر از CompositingStrategy تنظیمشده پرداز میکند.
خارج از صفحه
محتوای عنصر ترکیبی همیشه قبلاز پرداز کردن در مقصد، به بافت یا بیتمپ خارج از صفحه شطرنجی میشود. این ویژگی برای اعمال عملیات BlendMode روی محتوای پوششی و برای عملکرد هنگام پرداز کردن مجموعههای پیچیده دستورالعملهای طراحی مفید است.
نمونهای از استفاده از CompositingStrategy.Offscreen در BlendModes است.
در مثال زیر، فرض کنید میخواهید بخشهایی از یک Image
ترکیبشدنی را با صدور فرمان طراحی که از BlendMode.Clear استفاده میکند بردارید. اگر compositingStrategy را روی CompositingStrategy.Offscreen تنظیم نکنید،
BlendMode با همه محتوای پسزمینه تعامل میکند.
Image( painter = painterResource(id = R.drawable.dog), contentDescription = "Dog", contentScale = ContentScale.Crop, modifier = Modifier .size(120.dp) .aspectRatio(1f) .background( Brush.linearGradient( listOf( Color(0xFFC5E1A5), Color(0xFF80DEEA) ) ) ) .padding(8.dp) .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } .drawWithCache { val path = Path() path.addOval( Rect( topLeft = Offset.Zero, bottomRight = Offset(size.width, size.height) ) ) onDrawWithContent { clipPath(path) { // this draws the actual image - if you don't call drawContent, it wont // render anything this@onDrawWithContent.drawContent() } val dotSize = size.width / 8f // Clip a white border for the content drawCircle( Color.Black, radius = dotSize, center = Offset( x = size.width - dotSize, y = size.height - dotSize ), blendMode = BlendMode.Clear ) // draw the red circle indication drawCircle( Color(0xFFEF5350), radius = dotSize * 0.8f, center = Offset( x = size.width - dotSize, y = size.height - dotSize ) ) } } )
با تنظیم CompositingStrategy روی Offscreen، بافت خارج از صفحه
ایجاد میشود تا فرمانها را اجرا کند (BlendMode فقط روی
محتوای این عنصر ترکیبی اعمال میشود). سپس آن را روی آنچه ازقبل در صفحه
پردازش شده است پردازش میکند و بر محتوای ازقبل رسمشده تأثیر نمیگذارد.
اگر از CompositingStrategy.Offscreen استفاده نکرده باشید، نتایج اعمال
BlendMode.Clear همه پیکسلهای مقصد را پاک میکند، صرفنظر از
آنچه قبلاً تنظیم شده است - با این کار بافر پردازنده پنجره (سیاه)
نمایان میشود. بسیاری از BlendModes که شامل آلفا هستند بدون
بافر خارج از صفحه آنطور که انتظار میرود کار نخواهند کرد. به حلقه سیاه دور نشانگر دایره قرمز توجه کنید:
برای درک بیشتر این موضوع: اگر برنامه دارای پسزمینه پنجره نیمهشفاف باشد و از CompositingStrategy.Offscreen استفاده نکنید، BlendMode با کل برنامه تعامل خواهد داشت. همه پیکسلها را پاک میکند تا برنامه یا کاغذدیواری زیر را نشان دهد، مانند این مثال:
شایان ذکر است که هنگام استفاده از CompositingStrategy.Offscreen، بافت خارج از صفحهای به اندازه ناحیه طراحی ایجاد میشود و در صفحه رندر میشود. هر فرمان طراحی که با این استراتژی انجام شود، بهطور پیشفرض به این منطقه
چیده میشود. تکه کد زیر تفاوتها را هنگام تغییر به استفاده از بافتهای خارج از صفحه نشان میدهد:
@Composable fun CompositingStrategyExamples() { Column( modifier = Modifier .fillMaxSize() .wrapContentSize(Alignment.Center) ) { // Does not clip content even with a graphics layer usage here. By default, graphicsLayer // does not allocate + rasterize content into a separate layer but instead is used // for isolation. That is draw invalidations made outside of this graphicsLayer will not // re-record the drawing instructions in this composable as they have not changed Canvas( modifier = Modifier .graphicsLayer() .size(100.dp) // Note size of 100 dp here .border(2.dp, color = Color.Blue) ) { // ... and drawing a size of 200 dp here outside the bounds drawRect(color = Color.Magenta, size = Size(200.dp.toPx(), 200.dp.toPx())) } Spacer(modifier = Modifier.size(300.dp)) /* Clips content as alpha usage here creates an offscreen buffer to rasterize content into first then draws to the original destination */ Canvas( modifier = Modifier // force to an offscreen buffer .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) .size(100.dp) // Note size of 100 dp here .border(2.dp, color = Color.Blue) ) { /* ... and drawing a size of 200 dp. However, because of the CompositingStrategy.Offscreen usage above, the content gets clipped */ drawRect(color = Color.Red, size = Size(200.dp.toPx(), 200.dp.toPx())) } } }
ModulateAlpha
این استراتژی ترکیب مقدار آلفا را برای هریک از دستورالعملهای طراحی ضبطشده در graphicsLayer تغییر میدهد. برای آلفا کمتر از ۱٫۰f، بافر خارج از صفحه ایجاد نمیکند، مگر اینکه RenderEffect تنظیم شده باشد، بنابراین میتواند برای پرداز آلفا کارآمدتر باشد. بااینحال، ممکن است نتایج متفاوتی برای محتوای همپوشان ارائه دهد. برای موارد استفادهای که ازقبل مشخص است
که محتوا همپوشانی ندارد، این میتواند عملکرد بهتری نسبتبه
CompositingStrategy.Auto با مقادیر آلفای کمتر از ۱ ارائه دهد.
مثال زیر استراتژیهای ترکیببندی مختلف را نشان میدهد - اعمال کردن
آلفاهای مختلف روی بخشهای مختلف عناصر ترکیبشدنی، و اعمال کردن استراتژی
Modulate:
@Preview @Composable fun CompositingStrategy_ModulateAlpha() { Column( modifier = Modifier .fillMaxSize() .padding(32.dp) ) { // Base drawing, no alpha applied Canvas( modifier = Modifier.size(200.dp) ) { drawSquares() } Spacer(modifier = Modifier.size(36.dp)) // Alpha 0.5f applied to whole composable Canvas( modifier = Modifier .size(200.dp) .graphicsLayer { alpha = 0.5f } ) { drawSquares() } Spacer(modifier = Modifier.size(36.dp)) // 0.75f alpha applied to each draw call when using ModulateAlpha Canvas( modifier = Modifier .size(200.dp) .graphicsLayer { compositingStrategy = CompositingStrategy.ModulateAlpha alpha = 0.75f } ) { drawSquares() } } } private fun DrawScope.drawSquares() { val size = Size(100.dp.toPx(), 100.dp.toPx()) drawRect(color = Red, size = size) drawRect( color = Purple, size = size, topLeft = Offset(size.width / 4f, size.height / 4f) ) drawRect( color = Yellow, size = size, topLeft = Offset(size.width / 4f * 2f, size.height / 4f * 2f) ) } @Preview @Composable fun ModifierGraphicsLayerOutsets() { Column( modifier = Modifier .background(Color(0xFF0A0E14)) .padding(48.dp), verticalArrangement = Arrangement.spacedBy(48.dp), horizontalAlignment = Alignment.CenterHorizontally ) { val pillShape = RoundedCornerShape(percent = 50) // Without outsets: alpha = 0.5f creates an offscreen buffer that clips the 24.dp glow Box( modifier = Modifier .size( width = 140.dp, height = 56.dp ) .graphicsLayer( alpha = 0.5f, clip = false ) .dropShadow( shape = pillShape, shadow = Shadow(radius = 24.dp, color = Color.Cyan) ) .background(color = Color(0xFF141824), shape = pillShape), contentAlignment = Alignment.Center ) { Text( text = "✨ Glow", color = Color.White, fontSize = 18.sp ) } Box( modifier = Modifier .size( width = 140.dp, height = 56.dp ) .graphicsLayer( alpha = 0.5f, outsets = LayerOutsets(24.dp), clip = false ) .dropShadow( shape = pillShape, shadow = Shadow(radius = 24.dp, color = Color.Cyan) ) .background(color = Color(0xFF141824), shape = pillShape), contentAlignment = Alignment.Center ) { Text( text = "✨ Glow", color = Color.White, fontSize = 18.sp ) } } } val Purple = Color(0xFF7E57C2) val Yellow = Color(0xFFFFCA28) val Red = Color(0xFFEF5350)
نوشتن محتوای عنصر ترکیبی در بیتمپ
یک مورد استفاده رایج ایجاد Bitmap از یک عنصر ترکیبی است. برای کپی کردن محتوای
عناصر ترکیبی در Bitmap، بااستفاده از
rememberGraphicsLayer()، GraphicsLayer ایجاد کنید.
فرمانهای طراحی را بااستفاده از drawWithContent() و
graphicsLayer.record{} به لایه جدید هدایت کنید. سپس لایه را در بوم نمایان بااستفاده از
drawLayer بکشید:
val coroutineScope = rememberCoroutineScope() val graphicsLayer = rememberGraphicsLayer() Box( modifier = Modifier .drawWithContent { // call record to capture the content in the graphics layer graphicsLayer.record { // draw the contents of the composable into the graphics layer this@drawWithContent.drawContent() } // draw the graphics layer on the visible canvas drawLayer(graphicsLayer) } .clickable { coroutineScope.launch { val bitmap = graphicsLayer.toImageBitmap() // do something with the newly acquired bitmap } } .background(Color.White) ) { Text("Hello Android", fontSize = 26.sp) }
میتوانید بیتمپ را در دیسک ذخیره کنید و آن را همرسانی کنید. برای جزئیات بیشتر، گزیده نمونه کامل را ببینید. قبلاز تلاش برای ذخیره کردن در دیسک، حتماً اجازههای روی دستگاه را بررسی کنید.
اصلاحگر طراحی سفارشی
برای ایجاد اصلاحگر سفارشی خودتان، میانای DrawModifier را پیادهسازی کنید. این کار به شما امکان میدهد به ContentDrawScope دسترسی داشته باشید که همان چیزی است که هنگام استفاده از Modifier.drawWithContent() نمایان میشود. سپس میتوانید عملیات طراحی مشترک را به اصلاحکنندههای طراحی سفارشی استخراج کنید تا کد را پاکسازی کنید و پوششهای مناسبی ارائه دهید؛ برای مثال، Modifier.background() یک DrawModifier مناسب است.
برای مثال، اگر میخواهید Modifier را پیادهسازی کنید که محتوا را بهصورت عمودی برمیگرداند، میتوانید آن را بهصورت زیر ایجاد کنید:
class FlippedModifier : DrawModifier { override fun ContentDrawScope.draw() { scale(1f, -1f) { this@draw.drawContent() } } } fun Modifier.flipped() = this.then(FlippedModifier())
سپس از این تغییردهنده معکوسشده که روی Text اعمال شده است استفاده کنید:
Text( "Hello Compose!", modifier = Modifier .flipped() )
منابع بیشتر
برای مثالهای بیشتر درباره استفاده از graphicsLayer و طراحی سفارشی،
منابع زیر را بررسی کنید:
توصیهشده برای شما
- توجه: نوشتار پیوند وقتی جاوا اسکریپت خاموش است نمایش داده میشود
- گرافیک در Compose
- سفارشیسازی تصویر {:#customize-image}
- Kotlin برای Jetpack Compose