रिच टेक्स्ट इनपुट को स्टाइल करना

रिच टेक्स्ट इनपुट की मदद से, उपयोगकर्ता अपने टाइप किए गए टेक्स्ट को फ़ॉर्मैट कर सकते हैं. इसमें किसी शब्द को बोल्ड करना, हेडिंग जोड़ना, पैराग्राफ़ को अलाइन करना या हाइलाइट किया गया @mention शामिल करना शामिल है. Compose में, इसे स्टेट-आधारित टेक्स्ट फ़ील्ड (BasicTextField और TextField) के ऊपर बनाया जाता है. स्टाइल, टेक्स्ट के साथ TextFieldState में मौजूद होती हैं. इसलिए, बदलाव करने, चुने गए टेक्स्ट में बदलाव करने, और फिर से कंपोज़ करने पर भी वे बनी रहती हैं. साथ ही, उपयोगकर्ता के टाइप करने, मिटाने, और चिपकाने पर भी वे सिंक रहती हैं.

इस पेज पर, TextFieldBuffer का इस्तेमाल करके, TextFieldState में स्टाइल लागू करने, अपडेट करने, क्वेरी करने, एक्सट्रैक्ट करने, और हटाने का तरीका बताया गया है. साथ ही, 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.InsideOnly रेंज की शुरुआत या आखिर में टाइप किए गए टेक्स्ट को बिना स्टाइल के दिखाने वाला ऐनिमेशन.
ExpandPolicy.AtStart start पर टाइप किए गए टेक्स्ट में स्टाइल लागू हो जाता है. वहीं, end पर टाइप किए गए टेक्स्ट में स्टाइल लागू नहीं होता. स्टाइल किए गए टेक्स्ट के ठीक सामने टाइप किए गए टेक्स्ट में भी वही फ़ॉर्मैटिंग लागू होनी चाहिए. ऐनिमेशन में, ExpandPolicy.AtStart रेंज की शुरुआत में टाइप किया गया टेक्स्ट दिखाया गया है. इसमें स्टाइल को इनहेरिट किया गया है, जबकि आखिर में टाइप किए गए टेक्स्ट में कोई स्टाइल नहीं है.
ExpandPolicy.AtEnd end पर टाइप किए गए टेक्स्ट में स्टाइल लागू हो जाता है. वहीं, start पर टाइप किए गए टेक्स्ट में स्टाइल लागू नहीं होता. स्टैंडर्ड रिच टेक्स्ट एडिटर टाइपिंग, जिसमें बोल्ड या इटैलिक शब्द के बाद टाइप करना जारी रखने पर, वह फ़ॉर्मैटिंग जारी रहती है. ऐनिमेशन में, ExpandPolicy.AtEnd रेंज के आखिर में टाइप किया गया टेक्स्ट दिखाया गया है. इसमें स्टाइल को इनहेरिट किया गया है. वहीं, रेंज की शुरुआत में टाइप किए गए टेक्स्ट में कोई स्टाइल नहीं है.
ExpandPolicy.AtBoth start या end पर टाइप किया गया टेक्स्ट, रेंज को बढ़ाता है और स्टाइल को इनहेरिट करता है. ऐक्टिव हाइलाइट या लगातार फ़ॉर्मैट किए गए ब्लॉक, जो किसी भी किनारे से बढ़ने चाहिए. ऐनिमेशन में, ExpandPolicy.AtBoth रेंज की शुरुआत या आखिर में टाइप किया गया टेक्स्ट दिखाया गया है. यह टेक्स्ट, स्टाइल को इनहेरिट करता है.

SpanStyle की मदद से, वर्ण की फ़ॉर्मैटिंग लागू करना

बफ़र में किसी खास TextRange पर, टेक्स्ट का रंग, बैकग्राउंड हाइलाइट, FontWeight, FontStyle या अंडरलाइन जैसी वर्ण-लेवल की फ़ॉर्मैटिंग लागू करने के लिए, SpanStyle के साथ addStyle का इस्तेमाल करें.

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
                )
            }
        }
    }
)

एक TextField, जिसमें उपयोगकर्ता के टाइप करने पर, बोल्ड और इटैलिक लाल रंग की फ़ॉर्मैटिंग डाइनैमिक तौर पर लागू होती है.
पहली इमेज. उपयोगकर्ता के इनपुट को डाइनैमिक तौर पर बोल्ड और इटैलिक लाल रंग में फ़ॉर्मैट करना.

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 का इस्तेमाल करके, पैराग्राफ़ के अलाइनमेंट को शुरुआत, बीच, और आखिर के बीच टॉगल करने वाला TextField.
दूसरी इमेज. ParagraphStyle का इस्तेमाल करके, पैराग्राफ़ के अलाइनमेंट को शुरुआत, बीच, और आखिर में टॉगल करना.

ParagraphStyle का इस्तेमाल करते समय होने वाली आम गलतियों से बचना

ParagraphStyle, टेक्स्ट के पूरे ब्लॉक पर लागू होता है. इसलिए, इन मामलों में इसका इस्तेमाल करने से बचें:

  • आस-पास के पैराग्राफ़ में टेक्स्ट जोड़ना: ExpandPolicy को ध्यान से सेट करें, ताकि उपयोगकर्ता के बदलाव करने के बाद, आस-पास की ParagraphStyle रेंज ओवरलैप न हों. उदाहरण के लिए, अगर [0, 5), ExpandPolicy.AtEnd का इस्तेमाल करता है और [5, 10), ExpandPolicy.AtStart का इस्तेमाल करता है, तो इंडेक्स 5 पर टेक्स्ट डालने से दोनों स्टाइल ओवरलैप हो जाती हैं और IllegalArgumentException दिखता है.
  • ParagraphStyle रेंज को नेस्ट करना: हालांकि, AnnotatedString में पूरी तरह से नेस्ट की गई रेंज (जैसे कि [0, 10) और [2, 8)) मान्य हैं, लेकिन टेक्स्ट फ़ील्ड में ParagraphStyle रेंज को नेस्ट करने से बचें. Layout, नेस्ट की गई रेंज को अलग-अलग पैराग्राफ़ में बांट देता है. इसके बाद, किए गए बदलावों की वजह से, नेस्ट की गई रेंज अनजाने में कुछ हद तक ओवरलैप हो सकती हैं.

TrackedRange की मदद से, किसी स्टाइल को अपडेट करना

TextRange और ExpandPolicy के साथ addStyle को कॉल करने पर, TrackedRange हैंडल मिलता है. यह TrackedRange<SpanStyle> या TrackedRange<ParagraphStyle> हो सकता है. TrackedRange, TextFieldBuffer में स्टाइल इंटरवल का डाइनैमिक रेफ़रंस होता है. यह टेक्स्ट को शामिल करने, मिटाने या बदलने पर, अपने start और end वर्ण ऑफ़सेट को अपने-आप अपडेट करता है.

मौजूदा जगह पर बदलाव करने की क्षमता

किसी मौजूदा स्टाइल को अपडेट करने या उसकी सीमाओं में बदलाव करने के लिए, आपको removeStyle और addStyle को कॉल करने की ज़रूरत नहीं है. TextFieldBuffer स्कोप में, TrackedRange एक्सटेंशन प्रॉपर्टी उपलब्ध कराता है. ये प्रॉपर्टी, इंटरवल को अपडेट करती हैं. साथ ही, अन्य स्टाइल के हिसाब से, लेयरिंग के ओरिजनल ऑर्डर को बनाए रखती हैं:

  • TrackedRange<SpanStyle>.spanStyle: TrackedRange<SpanStyle> पर मौजूद SpanStyle पेलोड को पढ़ता है या बदलता है.
  • TrackedRange<ParagraphStyle>.paragraphStyle: यह TrackedRange<ParagraphStyle> पर मौजूद 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 ब्लॉक के अंदर: TextFieldBuffer रिसीवर पर सीधे getSpanStyles और getParagraphStyles को कॉल करें, ताकि म्यूटेबल TrackedRange हैंडल मिल सकें.
  • एडिट ब्लॉक के बाहर: TextFieldState.textStyles को रीड करें. इससे, ऐसी AnnotatedString.Range सूचियां मिलती हैं जिनमें बदलाव नहीं किया जा सकता. ये सूचियां, कमिट की गई स्थिति को दिखाती हैं.

TextFieldBuffer में इंटरसेक्ट करने वाली स्टाइल के लिए क्वेरी करना

TextFieldBuffer.getSpanStyles और TextFieldBuffer.getParagraphStyles, TrackedRange के उन सभी इंस्टेंस को दिखाते हैं जो दिए गए TextRange को इंटरसेक्ट करते हैं. इन्हें बफ़र में जोड़े जाने के क्रम में दिखाया जाता है.

उदाहरण के लिए, उपयोगकर्ता के मौजूदा चुने गए हिस्से को कवर करने वाली स्टाइल की जांच करने या उन्हें अपडेट करने के लिए, state.edit के अंदर getSpanStyles(selection) से क्वेरी की जा सकती है:

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) से मैच करता है.
    • Not returned (touching end rule): कर्सर की पोज़िशन पर खत्म होने वाली कोई भी स्टाइल. उदाहरण के लिए, 10 पर मौजूद कर्सर, [0, 10) को शामिल नहीं करता.

removeStyle की मदद से स्टाइल हटाएं या टॉगल करें

TextFieldBuffer से स्टाइल इंटरवल को मिटाने के लिए, removeStyle को TrackedRange हैंडल पास करें.

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
            )
        }
    }
}

एक TextField, जो चुने गए शब्दों के लिए बोल्ड फ़ॉर्मैटिंग को टॉगल करके चालू और बंद करता है.
तीसरी इमेज. टेक्स्ट को बोल्ड और अनबोल्ड करने के बीच टॉगल करना.

रिच टेक्स्ट को एक्सट्रैक्ट या सीरियलाइज़ करना

फ़ॉर्मैट किए गए इनपुट को सिर्फ़ पढ़ने के लिए उपलब्ध 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 में पूरा उदाहरण

इनलाइन वर्ण स्टाइलिंग (बोल्ड, इटैलिक, अंडरलाइन, स्ट्राइकथ्रू, और रंग) को जोड़ने वाले पूरे तरीके को देखने के लिए, GitHub पर Jetchat सैंपल देखें:

  • RichTextState.kt: इसमें TextFieldState स्टाइल क्वेरी, स्पैन टॉगल करना, चुने गए टेक्स्ट की सीमाओं के हिसाब से ट्रिम करना या स्प्लिट करना वगैरह शामिल है.
  • UserInput.kt: यह RichTextState को रिच टेक्स्ट फ़ॉर्मैटिंग टूलबार और मैसेज इनपुट फ़ील्ड से कनेक्ट करता है.
Jetchat सैंपल के तौर पर मिला ऐप्लिकेशन, जिसमें मैसेज इनपुट बार में रिच टेक्स्ट फ़ॉर्मैटिंग दिखाई गई है.
चौथी इमेज. Jetchat में रिच टेक्स्ट एडिटिंग.