ورودی نوشتار غنیشده به کاربران اجازه میدهد آنچه را تایپ میکنند قالببندی کنند، ازجمله پررنگ کردن کلمه،
افزودن عنوان، تراز کردن پاراگراف، یا درج کردن @mention برجستهشده.
در Compose، این را روی فیلدهای نوشتاری مبتنی بر وضعیت میسازید
(BasicTextField و TextField). سبکها در
TextFieldState در کنار نوشتار قرار دارند، بنابراین در ویرایشها، تغییرات انتخاب،
و ترکیببندیهای مجدد باقی میمانند و با تایپ کردن، حذف کردن، و جایگذاری کردن کاربر
همگام میمانند.
این صفحه نشان میدهد که چگونه میتوان سبکها را در
TextFieldState بااستفاده از TextFieldBuffer اعمال، بهروزرسانی، پُرسمان، استخراج، و حذف کرد، و چگونه میتوان یک نوارابزار قالببندی را از TextFieldState.textStyles هدایت کرد.
اعمال سبکهای درونخطی و پاراگراف
میتوانید قالببندی سطح نویسه و سطح پاراگراف را در هر
TextFieldBuffer دامنه گیرنده، ازجمله TextFieldState.edit،
InputTransformation.transformInput، و
OutputTransformation.transformOutput اعمال کنید.
کنترل گسترش مرز با ExpandPolicy
وقتی addStyle را فراخوانی میکنید، میتوانید ExpandPolicy را ارسال کنید تا تعریف کنید که
محدوده سبکدار وقتی کاربر نویسههای جدید را مستقیماً در مرز
start یا end آن درج میکند، چگونه رفتار کند:
- درج در داخل
(start, end): همیشه گستره سبکدار را گسترش میدهد. - درج قبلاز
start: همیشه هر دو جابهجاییstartوendرا به راست منتقل میکند. - درج مستقیم در
startیاend: توسطExpandPolicyکه مشخص میکنید کنترل میشود.
جدول زیر گزینههای ExpandPolicy دردسترس و موارد استفاده معمول آنها را شرح میدهد:
| خطمشی | رفتار مرزی | موارد استفاده توصیهشده | مثال |
|---|---|---|---|
ExpandPolicy.InsideOnly |
نوشتار تایپشده در start یا end خارج از محدوده سبکدار باقی میماند. |
نشانهای گسسته مانند @mentions، #hashtags، یا قرصهای پسزمینه که تایپ کردن در کنار برچسب نباید رنگ پسزمینه برچسب را به ارث ببرد. |
![]() |
ExpandPolicy.AtStart |
نوشتار تایپشده در start سبک را به ارث میبرد و نوشتار تایپشده در end بدون سبک باقی میماند. |
نوشتاری که مستقیماً جلوی نوشتار سبکدار تایپ میشود نیز باید همان قالببندی را داشته باشد. | ![]() |
ExpandPolicy.AtEnd |
نوشتار تایپشده در end سبک را به ارث میبرد و نوشتار تایپشده در start بدون سبک باقی میماند. |
تایپ کردن در ویرایشگر نوشتار غنیشده استاندارد، که در آن ادامه تایپ کردن بعداز کلمه پررنگ یا کج، آن قالببندی را ادامه میدهد. | ![]() |
ExpandPolicy.AtBoth |
نوشتار تایپشده در start یا end گستره را گسترش میدهد و سبک را بهارث میبرد. |
برجستهسازیهای فعال یا بلوکهای قالببندیشده پیوسته که باید از هریک از لبهها رشد کنند. | ![]() |
اعمال قالببندی نویسه با SpanStyle
از addStyle با SpanStyle برای اعمال قالببندی در سطح نویسه،
مثل رنگ نوشتار، برجستهسازی پسزمینه، FontWeight، FontStyle،
یا زیرخطدار کردن TextRange خاصی در میانگیر استفاده کنید.
رفتارهای کلیدی برای محدودههای SpanStyle
- ترتیب لایهبندی: سبکها بهترتیبی که آنها را به میانگیر اضافه میکنید اعمال میشوند. اگر
چند محدوده
SpanStyleهمپوشان ویژگی یکسانی را با مقادیر متفاوت تنظیم کنند، سبکی که دیرتر اضافه شده است در بالا نمایش داده میشود. - الزام محدوده غیرخالی: باید محدوده جمعنشدهای را ارسال کنید که در آن
start<endباشد. فراخوانیaddStyleدر انتخاب جمعشده با طول0تأثیری ندارد و ارسال نمایههای معکوس یا خارج از محدوده باعث ایجادIllegalArgumentExceptionمیشود.
TextField( state = rememberTextFieldState(), inputTransformation = InputTransformation { for (i in 0 until changes.changeCount) { val range = changes.getRange(i) if (!range.collapsed) { addStyle( spanStyle = SpanStyle( fontWeight = FontWeight.ExtraBold, fontStyle = FontStyle.Italic, color = Color.Red, ), range = range, expandPolicy = ExpandPolicy.AtEnd ) } } } )
اعمال قالببندی بلوک با ParagraphStyle
از addStyle با ParagraphStyle برای اعمال قالببندی ساختاری در سطح بلوک مانند textAlign، lineHeight، textDirection، یا textIndent به محدوده پاراگراف استفاده کنید.
fun setParagraphAlignment(state: TextFieldState, alignment: TextAlign) { val targetRange = if (state.selection.collapsed) { TextRange(0, state.text.length) } else { state.selection } if (targetRange.collapsed) return state.edit { // 1. Remove existing paragraph styles in this range getParagraphStyles(targetRange).forEach { removeStyle(it) } // 2. Apply the new paragraph alignment addStyle( paragraphStyle = ParagraphStyle(textAlign = alignment), range = targetRange, expandPolicy = ExpandPolicy.InsideOnly ) } }
ParagraphStyle.مشکلاتی که باید با ParagraphStyle از آنها اجتناب کنید
چون ParagraphStyle برای کل بلوکهای نوشتار اعمال میشود، از آن در سناریوهای زیر استفاده نکنید:
- گسترش به پاراگرافهای مجاور:
ExpandPolicyرا با دقت تنظیم کنید تا محدودههایParagraphStyleمجاور پساز ویرایشهای کاربر همپوشانی نداشته باشند. برای مثال، اگر[0, 5)ازExpandPolicy.AtEndاستفاده کند و[5, 10)ازExpandPolicy.AtStartاستفاده کند، درج نوشتار در نمایه5هر دو سبک را به همپوشانی گسترش میدهد وIllegalArgumentExceptionرا برمیگرداند. - محدودههای
ParagraphStyleتودرتو: اگرچه محدودههای کاملاً تودرتو (مانند[0, 10)و[2, 8)) درAnnotatedStringمعتبر هستند، از تودرتو کردن محدودههایParagraphStyleدر فیلد نوشتار خودداری کنید. «چیدمان» محدودههای تودرتو را به پاراگرافهای جداگانه تقسیم میکند و ویرایشهای بعدی میتواند بهطور ناخواسته محدودههای تودرتو را به همپوشانی جزئی تبدیل کند.
بهروزرسانی سبک در مکان با TrackedRange
فراخوانی addStyle با TextRange و ExpandPolicy،
TrackedRange را برمیگرداند که میتواند TrackedRange<SpanStyle> یا
TrackedRange<ParagraphStyle> باشد. TrackedRange ارجاعی پویا به
فاصله سبک در TextFieldBuffer است که بهطور خودکار start
و end جابهجایی نویسه را با درج، حذف، یا جایگزین شدن نوشتار در اطراف آن بهروز میکند.
تغییرپذیری درجا
برای بهروزرسانی سبک موجود یا اصلاح مرزهای آن، لازم نیست removeStyle و addStyle را فراخوانی کنید. درون محدوده TextFieldBuffer، TrackedRange
ویژگیهای افزونهای ارائه میدهد که فاصله را در محل بهروزرسانی میکند و درعینحال
ترتیب لایهبندی اصلی آن را نسبتبه سبکهای دیگر حفظ میکند:
TrackedRange<SpanStyle>.spanStyle: محمولهSpanStyleرا در محلTrackedRange<SpanStyle>میخواند یا جایگزین میکند.TrackedRange<ParagraphStyle>.paragraphStyle: محمولهParagraphStyleرا درTrackedRange<ParagraphStyle>میخواند یا جایگزین میکند.TrackedRange<*>.textRange: مرزهایTextRangeفعلی را در محل میخواند یا بهروز میکند. تنظیم کردن محدوده جمعشده، معکوس، یا خارجاز محدوده،IllegalArgumentExceptionرا برمیگرداند.TrackedRange<*>.expandPolicy:ExpandPolicyرا در محل میخواند یا بهروز میکند.TrackedRange<*>.isValid: اگر فاصله سبک هنوز در میانگیر فعال باشد،trueرا برمیگرداند، یا اگر ویرایشها باعث شده باشد طول آن به0کاهش یابد یا اگر برداشته شده باشد،falseرا برمیگرداند.
// Trim an existing bold span in place when the selection overlaps one edge val range = style.textRange if (range.start < selection.min && range.end <= selection.max) { // Selection covers the end of the span - shrink end boundary in place style.textRange = TextRange(range.start, selection.min) } else if (range.start >= selection.min && range.end > selection.max) { // Selection covers the start of the span - shrink start boundary in place style.textRange = TextRange(selection.max, range.end) }
قوانین چرخه حیات و محدودیت دامنه
هنگام کار با نمونههای TrackedRange، قوانین دامنه و چرخه حیات زیر را درنظر داشته باشید:
- محدود به
TextFieldBuffer: ویژگیهای افزونهTrackedRangeفقط در محدودهTextFieldBufferکه دسته ایجاد یا پرسمان شده است وجود دارد. - هرس خودکار هنگام جمع شدن: اگر ویرایشهای کاربر همه نویسههای تحت پوشش
TrackedRangeرا حذف کند، طول آن به0کاهش مییابد،isValidمقدارfalseرا برمیگرداند، و سبک از میانگیر برداشته میشود.
پرسوجو و برداشتن سبکها
Compose دو روش برای بررسی سبکهای فعال ارائه میدهد، بسته به اینکه آیا میانگیر را تغییر میدهید یا حالت را برای واسط کاربر میخوانید:
- داخل بلوک
TextFieldBuffer:getSpanStylesوgetParagraphStylesرا مستقیماً در گیرندهTextFieldBufferفراخوانی کنید تا دستههایTrackedRangeتغییرپذیر را دریافت کنید. - خارج از بلوک ویرایش:
TextFieldState.textStylesرا بخوانید که فهرستهای تغییرناپذیرAnnotatedString.Rangeرا که نشاندهنده وضعیت تعهدشده است برمیگرداند.
پُرسمان سبکهای متقاطع در TextFieldBuffer
TextFieldBuffer.getSpanStyles و
TextFieldBuffer.getParagraphStyles همه نمونههای TrackedRange را که با TextRange دادهشده تلاقی دارند، بهترتیب زمانی که به میانگیر اضافه شدهاند، برمیگردانند.
برای مثال، میتوانید getSpanStyles(selection) را در state.edit پُرسمان کنید تا
سبکهای پوششدهنده انتخاب فعلی کاربر را بازرسی یا بهروزرسانی کنید:
state.edit { if (selection.collapsed) return@edit // Query all bold spans that overlap with the current selection val boldStyles: List<TrackedRange<SpanStyle>> = getSpanStyles(selection).filter { it.spanStyle.fontWeight == FontWeight.Bold } // ... }
فاصلههای سبک از محدوده ریاضی نیمهباز [start, end) استفاده میکنند. اینکه سبک لمس برگردانده شود یا نه بستگی به این دارد که شما یک انتخاب را پُرسمان کنید یا یک نقطه مکاننما را:
- برای انتخاب غیرخالی
[start, end):- برگرداندهشده: هر سبکی که با محدوده انتخاب همپوشانی داشته باشد، آن را دربرگیرد، یا در آن قرار داشته باشد. برای مثال، پرسمان
[5, 15)با[0, 20)،[8, 12)، یا[0, 10)مطابقت دارد. - برگردانده نشده: سبکهایی که فقط لبههای مرز بیرونی را لمس میکنند بدون اینکه با نویسهها همپوشانی داشته باشند. برای مثال، پُرسمان
[5, 15)شامل[0, 5)و[15, 25)نمیشود.
- برگرداندهشده: هر سبکی که با محدوده انتخاب همپوشانی داشته باشد، آن را دربرگیرد، یا در آن قرار داشته باشد. برای مثال، پرسمان
- برای مکاننمای جمعشده
[index, index]:- برگرداندهشده: هر سبکی که حاوی مکاننما باشد یا از موقعیت مکاننما شروع شود. برای مثال، مکاننما در
10با[0, 20)و[10, 20)مطابقت دارد. - برگردانده نشده (قانون لمس کردن پایان): هر سبکی که در موقعیت مکاننما بهپایان میرسد. برای مثال، مکاننما در
10،[0, 10)را مستثنی میکند.
- برگرداندهشده: هر سبکی که حاوی مکاننما باشد یا از موقعیت مکاننما شروع شود. برای مثال، مکاننما در
برداشتن یا تغییر دادن سبکها با removeStyle
یک دسته TrackedRange را به removeStyle منتقل کنید تا آن بازه سبک خاص را از TextFieldBuffer حذف کنید.
removeStyle true را برمیگرداند اگر TrackedRange پیدا و برداشته شده باشد، یا
false اگر قبلاً برداشته شده باشد یا به میانگیر دیگری تعلق داشته باشد. این کار فقط دسته TrackedRange هدف را برمیدارد و سایر سبکهای همپوشان را دستنخورده باقی میگذارد.
fun toggleBold(state: TextFieldState) { state.edit { if (selection.collapsed) return@edit // Query all bold spans that overlap with the current selection val boldStyles: List<TrackedRange<SpanStyle>> = getSpanStyles(selection).filter { it.spanStyle.fontWeight == FontWeight.Bold } if (boldStyles.isNotEmpty()) { // Remove or trim existing bold spans covering the selection for (style in boldStyles) { val range = style.textRange if (range.start >= selection.min && range.end <= selection.max) { removeStyle(style) } else if (range.start < selection.min && range.end > selection.max) { val oldEnd = range.end style.textRange = TextRange(range.start, selection.min) addStyle( spanStyle = SpanStyle(fontWeight = FontWeight.Bold), range = TextRange(selection.max, oldEnd), expandPolicy = ExpandPolicy.AtEnd ) } else if (range.start < selection.min) { style.textRange = TextRange(range.start, selection.min) } else { style.textRange = TextRange(selection.max, range.end) } } } else { // No bold spans intersect the selection - apply bold addStyle( spanStyle = SpanStyle(fontWeight = FontWeight.Bold), range = selection, expandPolicy = ExpandPolicy.AtEnd ) } } }
استخراج یا سریالسازی نوشتار غنیشده
برای نمایش ورودی قالببندیشده در Text ترکیبپذیر فقط خواندنی، یا سریالسازی آن در
سرویس پسبک، TextFieldState.textStyles را خارج از بلوک ویرایش بخوانید.
state.textStyles.getSpanStyles و
state.textStyles.getParagraphStyles
List<AnnotatedString.Range<SpanStyle>> و
List<AnnotatedString.Range<ParagraphStyle>> را برمیگردانند که میتوانید مستقیماً به
سازنده AnnotatedString ارسال کنید:
fun TextFieldState.toAnnotatedString(): AnnotatedString { val fullRange = TextRange(0, text.length) return AnnotatedString( text = text.toString(), spanStyles = textStyles.getSpanStyles(fullRange), paragraphStyles = textStyles.getParagraphStyles(fullRange) ) }
نمونه کامل در Jetchat
برای دیدن پیادهسازی کاملی که سبکبندی نویسه درونخطی (پررنگ، ایتالیک، زیرخط، خطزده، و رنگ) را ترکیب میکند، نمونه Jetchat را در GitHub ببینید:
RichTextState.kt: پرسوجوهای سبکTextFieldState، تغییر وضعیت دهانه، پیرایش یا تقسیم در مرزهای انتخاب، و غیره را کپسولهسازی میکند.UserInput.kt:RichTextStateرا به نوارابزار قالببندی متن غنی و فیلد ورودی پیام متصل میکند.



