Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ You can find some examples in the [usage section](#usage) or in the example app.
- [onLinkDetected](docs/API_REFERENCE.md#onlinkdetected) - returns link's detailed info whenever user selection is near one.
- [onMentionDetected](docs/API_REFERENCE.md#onmentiondetected) - returns mention's detailed info whenever user selection is near one.
- [onKeyPress](docs/API_REFERENCE.md#onkeypress) - emits whenever a key is pressed. Follows react-native TextInput's onKeyPress event [spec](https://reactnative.dev/docs/textinput#onkeypress).
- [onPasteImages](docs/API_REFERENCE.md#onpasteimages) - returns an array of images details whenever an image/GIF is pasted into the input.

## Customizing \<EnrichedTextInput /> styles

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.view.ViewCompat
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.ReactConstants
Expand All @@ -33,7 +36,9 @@ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
import com.swmansion.enriched.textinput.events.MentionHandler
import com.swmansion.enriched.textinput.events.OnInputBlurEvent
import com.swmansion.enriched.textinput.events.OnInputFocusEvent
import com.swmansion.enriched.textinput.events.OnPasteImagesEvent
import com.swmansion.enriched.textinput.events.OnRequestHtmlResultEvent
import com.swmansion.enriched.textinput.events.PastedImage
import com.swmansion.enriched.textinput.spans.EnrichedH1Span
import com.swmansion.enriched.textinput.spans.EnrichedH2Span
import com.swmansion.enriched.textinput.spans.EnrichedH3Span
Expand All @@ -53,6 +58,7 @@ import com.swmansion.enriched.textinput.utils.EnrichedEditableFactory
import com.swmansion.enriched.textinput.utils.EnrichedParser
import com.swmansion.enriched.textinput.utils.EnrichedSelection
import com.swmansion.enriched.textinput.utils.EnrichedSpanState
import com.swmansion.enriched.textinput.utils.RichContentReceiver
import com.swmansion.enriched.textinput.utils.mergeSpannables
import com.swmansion.enriched.textinput.watchers.EnrichedSpanWatcher
import com.swmansion.enriched.textinput.watchers.EnrichedTextWatcher
Expand Down Expand Up @@ -136,6 +142,11 @@ class EnrichedTextInputView : AppCompatEditText {

init {
inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
ViewCompat.setOnReceiveContentListener(
this,
RichContentReceiver.MIME_TYPES,
RichContentReceiver(this, context as ReactContext),
)
}

private fun prepareComponent() {
Expand Down Expand Up @@ -228,11 +239,6 @@ class EnrichedTextInputView : AppCompatEditText {
handleCustomCopy()
return true
}

android.R.id.paste -> {
handleCustomPaste()
return true
}
}
return super.onTextContextMenuItem(id)
}
Expand All @@ -252,16 +258,11 @@ class EnrichedTextInputView : AppCompatEditText {
}
}

private fun handleCustomPaste() {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
if (!clipboard.hasPrimaryClip()) return

val clip = clipboard.primaryClip
val item = clip?.getItemAt(0)
val htmlText = item?.htmlText
fun handleTextPaste(item: ClipData.Item) {
val htmlText = item.htmlText
val currentText = text as Spannable
val start = selection?.start ?: 0
val end = selection?.end ?: 0
val start = selectionStart.coerceAtLeast(0)
val end = selectionEnd.coerceAtLeast(0)

if (htmlText != null) {
val parsedText = parseText(htmlText)
Expand All @@ -272,8 +273,7 @@ class EnrichedTextInputView : AppCompatEditText {
}
}

// Currently, we do not support pasting images
if (item?.text == null) return
if (item.text == null) return
val lengthBefore = currentText.length
val finalText = currentText.mergeSpannables(start, end, item.text.toString())
setValue(finalText)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.swmansion.enriched.textinput.events.OnInputKeyPressEvent
import com.swmansion.enriched.textinput.events.OnLinkDetectedEvent
import com.swmansion.enriched.textinput.events.OnMentionDetectedEvent
import com.swmansion.enriched.textinput.events.OnMentionEvent
import com.swmansion.enriched.textinput.events.OnPasteImagesEvent
import com.swmansion.enriched.textinput.events.OnRequestHtmlResultEvent
import com.swmansion.enriched.textinput.spans.EnrichedSpans
import com.swmansion.enriched.textinput.styles.HtmlStyle
Expand Down Expand Up @@ -72,6 +73,7 @@ class EnrichedTextInputViewManager :
map.put(OnChangeSelectionEvent.EVENT_NAME, mapOf("registrationName" to OnChangeSelectionEvent.EVENT_NAME))
map.put(OnRequestHtmlResultEvent.EVENT_NAME, mapOf("registrationName" to OnRequestHtmlResultEvent.EVENT_NAME))
map.put(OnInputKeyPressEvent.EVENT_NAME, mapOf("registrationName" to OnInputKeyPressEvent.EVENT_NAME))
map.put(OnPasteImagesEvent.EVENT_NAME, mapOf("registrationName" to OnPasteImagesEvent.EVENT_NAME))

return map
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.swmansion.enriched.textinput.events

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableArray
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event

data class PastedImage(
val uri: String,
val type: String,
val width: Double,
val height: Double,
)

class OnPasteImagesEvent(
surfaceId: Int,
viewId: Int,
private val images: List<PastedImage>,
private val experimentalSynchronousEvents: Boolean,
) : Event<OnPasteImagesEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME

override fun getEventData(): WritableMap {
val imagesArray: WritableArray = Arguments.createArray()

for (image in images) {
val imageMap = Arguments.createMap()
imageMap.putString("uri", image.uri)
imageMap.putString("type", image.type)
imageMap.putDouble("width", image.width)
imageMap.putDouble("height", image.height)

imagesArray.pushMap(imageMap)
}

val eventData: WritableMap = Arguments.createMap()
eventData.putArray("images", imagesArray)

return eventData
}

override fun experimental_isSynchronous(): Boolean = experimentalSynchronousEvents

companion object {
const val EVENT_NAME: String = "onPasteImages"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package com.swmansion.enriched.textinput.spans
import android.content.res.Resources
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.ImageDecoder
import android.graphics.Paint
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.AnimatedImageDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import android.text.Editable
import android.text.Spannable
import android.text.style.ImageSpan
Expand All @@ -18,6 +20,7 @@ import com.swmansion.enriched.textinput.spans.interfaces.EnrichedInlineSpan
import com.swmansion.enriched.textinput.spans.utils.ForceRedrawSpan
import com.swmansion.enriched.textinput.styles.HtmlStyle
import com.swmansion.enriched.textinput.utils.AsyncDrawable
import java.io.File

class EnrichedImageSpan :
ImageSpan,
Expand Down Expand Up @@ -136,7 +139,7 @@ class EnrichedImageSpan :
width: Int,
height: Int,
): EnrichedImageSpan {
var imgDrawable = prepareDrawableForImage(src)
var imgDrawable = prepareDrawableForImage(src, width, height)

if (imgDrawable == null) {
imgDrawable = ResourceManager.getDrawableResource(R.drawable.broken_image)
Expand All @@ -145,7 +148,11 @@ class EnrichedImageSpan :
return EnrichedImageSpan(imgDrawable, src, width, height)
}

private fun prepareDrawableForImage(src: String): Drawable? {
private fun prepareDrawableForImage(
src: String,
width: Int,
height: Int,
): Drawable? {
var cleanPath = src

if (cleanPath.startsWith("http://") || cleanPath.startsWith("https://")) {
Expand All @@ -156,22 +163,41 @@ class EnrichedImageSpan :
cleanPath = cleanPath.substring(7)
}

var drawable: BitmapDrawable? = null
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return try {
val bitmap = BitmapFactory.decodeFile(cleanPath) ?: return null
val drawable = bitmap.toDrawable(Resources.getSystem())
drawable.setBounds(0, 0, bitmap.width, bitmap.height)
return drawable
} catch (e: Exception) {
Log.e("EnrichedImageSpan", "Failed to load legacy image: $cleanPath", e)
null
}
}

try {
val bitmap = BitmapFactory.decodeFile(cleanPath)
if (bitmap != null) {
drawable = bitmap.toDrawable(Resources.getSystem())
// set bounds so it knows how big it is naturally,
// though EnrichedImageSpan will override this with the HTML width/height later.
drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight())
return try {
val file = File(cleanPath)
val source = ImageDecoder.createSource(file)

val density = Resources.getSystem().displayMetrics.density
val targetWidthPx = (width * density).toInt()
val targetHeightPx = (height * density).toInt()

val drawable =
ImageDecoder.decodeDrawable(source) { decoder, info, source ->
decoder.setTargetSize(targetWidthPx, targetHeightPx)
}

if (drawable is AnimatedImageDrawable) {
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
drawable.repeatCount = AnimatedImageDrawable.REPEAT_INFINITE
drawable.start()
}
drawable
} catch (e: Exception) {
// Failed to load file
Log.e("EnrichedImageSpan", "Failed to load image from path: $cleanPath", e)
Log.e("EnrichedImageSpan", "Failed to load image: $cleanPath", e)
null
}

return drawable
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.ImageDecoder
import android.graphics.PixelFormat
import android.graphics.drawable.AnimatedImageDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.graphics.drawable.toDrawable
import com.swmansion.enriched.R
import com.swmansion.enriched.common.ResourceManager
import java.net.URL
import java.nio.ByteBuffer
import java.util.concurrent.Executors

class AsyncDrawable(
Expand All @@ -35,13 +39,12 @@ class AsyncDrawable(
try {
isLoaded = false
val inputStream = URL(url).openStream()
val bitmap = BitmapFactory.decodeStream(inputStream)
val bytes = inputStream.readBytes()
val d = prepareDrawable(bytes)

// Switch to Main Thread to update UI
mainHandler.post {
if (bitmap != null) {
val d = bitmap.toDrawable(Resources.getSystem())

if (d != null) {
d.bounds = bounds
internalDrawable = d
} else {
Expand All @@ -59,6 +62,38 @@ class AsyncDrawable(
}
}

private fun prepareDrawable(bytes: ByteArray): Drawable? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
try {
val buffer = ByteBuffer.wrap(bytes)
val source = ImageDecoder.createSource(buffer)

val drawable =
ImageDecoder.decodeDrawable(source) { decoder, _, _ ->
decoder.setTargetSize(bounds.width(), bounds.height())
}

if (drawable is AnimatedImageDrawable) {
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
drawable.repeatCount = AnimatedImageDrawable.REPEAT_INFINITE
drawable.start()
}

return drawable
} catch (e: Exception) {
Log.w("AsyncDrawable", "ImageDecoder failed, falling back to Bitmap", e)
}
}

// Fallback to bitmap if ImageDecoder fails
return try {
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
bitmap?.toDrawable(Resources.getSystem())
} catch (_: Exception) {
null
}
}

private fun loadPlaceholderImage() {
internalDrawable = ResourceManager.getDrawableResource(R.drawable.broken_image)
}
Expand Down
Loading