Comparing two VectorDrawables fails in Kaspresso - Android - android

I'm trying to use Kaspresso for tests and I'm checking whether a view has a certain drawable with the method:
withId<KImageView>(R.id.criticalErrorImage) {
hasDrawable(R.drawable.error_graphic)
}
With PNG works really well, while comparing the image stored in the imageView and restored and the VectorDrawable fails.
The file where the check is made is this one.
In particular this part of code:
var expectedDrawable: Drawable? = drawable ?: getResourceDrawable(resId)?.mutate()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && expectedDrawable != null) {
expectedDrawable = DrawableCompat.wrap(expectedDrawable).mutate()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
tintColorId?.let { tintColorId ->
val tintColor = getResourceColor(tintColorId)
expectedDrawable?.apply {
setTintList(ColorStateList.valueOf(tintColor))
setTintMode(PorterDuff.Mode.SRC_IN)
}
}
}
if (expectedDrawable == null) {
return false
}
val convertDrawable = (imageView as ImageView).drawable.mutate()
val bitmap = toBitmap?.invoke(convertDrawable) ?: convertDrawable.toBitmap()
val otherBitmap = toBitmap?.invoke(expectedDrawable) ?: expectedDrawable.toBitmap()
The funny part is that it works if I'm setting the image through databinding, while it doesn't work if I set it in all other ways. Cannot understand why.
I've created a POC here: https://github.com/filnik/proof_of_concept
In particular, here is the test: https://github.com/filnik/proof_of_concept/blob/master/app/src/androidTest/java/com/neato/test/POCInstrumentationTest.kt
While here I dynamically set the image:
https://github.com/filnik/proof_of_concept/blob/master/app/src/main/java/com/neato/test/FirstFragment.kt

The issue was because actually the image is scaled. So the scaled image is different from the original one.
To avoid this issue I've used this "altered" KImageView:
package your_package
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.PorterDuff
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.StateListDrawable
import android.os.Build
import android.view.View
import android.widget.ImageView
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.core.graphics.drawable.DrawableCompat
import androidx.test.espresso.DataInteraction
import androidx.test.espresso.assertion.ViewAssertions
import com.agoda.kakao.common.assertions.BaseAssertions
import com.agoda.kakao.common.builders.ViewBuilder
import com.agoda.kakao.common.utilities.getResourceColor
import com.agoda.kakao.common.utilities.getResourceDrawable
import com.agoda.kakao.common.views.KBaseView
import com.agoda.kakao.image.KImageView
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.TypeSafeMatcher
class KImageView2 : KBaseView<KImageView>, ImageViewAssertions2 {
constructor(function: ViewBuilder.() -> Unit) : super(function)
constructor(parent: Matcher<View>, function: ViewBuilder.() -> Unit) : super(parent, function)
constructor(parent: DataInteraction, function: ViewBuilder.() -> Unit) : super(parent, function)
}
interface ImageViewAssertions2 : BaseAssertions {
/**
* Checks if the view displays given drawable
*
* #param resId Drawable resource to be matched
* #param toBitmap Lambda with custom Drawable -> Bitmap converter (default is null)
*/
fun hasDrawable(#DrawableRes resId: Int, toBitmap: ((drawable: Drawable) -> Bitmap)? = null) {
view.check(ViewAssertions.matches(DrawableMatcher2(resId = resId, toBitmap = toBitmap)))
}
}
class DrawableMatcher2(
#DrawableRes private val resId: Int = -1,
#ColorRes private val tintColorId: Int? = null,
private val toBitmap: ((drawable: Drawable) -> Bitmap)? = null
) : TypeSafeMatcher<View>(View::class.java) {
override fun describeTo(desc: Description) {
desc.appendText("with drawable id $resId or provided instance")
}
override fun matchesSafely(view: View?): Boolean {
if (view !is ImageView) {
return false
}
if (resId < 0) {
return view.drawable == null
}
val bitmap = extractFromImageView(view)
view.setImageResource(resId)
val otherBitmap = extractFromImageView(view)
return bitmap?.sameAs(otherBitmap) ?: false
}
private fun extractFromImageView(view: View?): Bitmap? {
return view?.let { imageView ->
var expectedDrawable: Drawable? = getResourceDrawable(resId)?.mutate()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && expectedDrawable != null) {
expectedDrawable = DrawableCompat.wrap(expectedDrawable).mutate()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
tintColorId?.let { tintColorId ->
val tintColor = getResourceColor(tintColorId)
expectedDrawable?.apply {
setTintList(ColorStateList.valueOf(tintColor))
setTintMode(PorterDuff.Mode.SRC_IN)
}
}
}
if (expectedDrawable == null) {
return null
}
val convertDrawable = (imageView as ImageView).drawable.mutate()
toBitmap?.invoke(convertDrawable) ?: convertDrawable.toBitmap()
}
}
}
internal fun Drawable.toBitmap(): Bitmap {
if (this is BitmapDrawable && this.bitmap != null) {
return this.bitmap
}
if (this is StateListDrawable && this.getCurrent() is BitmapDrawable) {
val bitmapDrawable = this.getCurrent() as BitmapDrawable
if (bitmapDrawable.bitmap != null) {
return bitmapDrawable.bitmap
}
}
val bitmap = if (this.intrinsicWidth <= 0 || this.intrinsicHeight <= 0) {
Bitmap.createBitmap(
1,
1,
Bitmap.Config.ARGB_8888
) // Single color bitmap will be created of 1x1 pixel
} else {
Bitmap.createBitmap(this.intrinsicWidth, this.intrinsicHeight, Bitmap.Config.ARGB_8888)
}
val canvas = Canvas(bitmap)
this.setBounds(0, 0, canvas.width, canvas.height)
this.draw(canvas)
return bitmap
}
Not the cleanest solution possible, but it works. If anyone can provide a better solution would be great :-)

Related

Mapbox Fixed Location in Android

How do I show a location marker for a fixed location? All am getting from the docs is user location or current location. The task I want to achieve is to get coordinates of a certain location and have a marker appear at that particular location.
Below is the code I tried with but the marker appears somewhere else:
package com.show.show_map
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import com.mapbox.geojson.Point
import com.mapbox.maps.MapView
import com.mapbox.maps.Style
import com.mapbox.maps.plugin.annotation.annotations
import com.mapbox.maps.plugin.annotation.generated.PointAnnotationOptions
import com.mapbox.maps.plugin.annotation.generated.createPointAnnotationManager
import okhttp3.logging.HttpLoggingInterceptor
class AbsaLocationActivity : AppCompatActivity() {
private var mapView: MapView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_absa_location)
val BASIC = "mapbox://styles/stunupps/cl2f3q6b1002d14o6wpu3m6bq"
mapView = findViewById(R.id.mapView)
mapView?.getMapboxMap()?.loadStyleUri(
HttpLoggingInterceptor.Level.BASIC,
object : Style.OnStyleLoaded {
override fun onStyleLoaded(style: Style) {
addAnnotationToMap()
}
}
)
}
private fun addAnnotationToMap() {
// Create an instance of the Annotation API and get the PointAnnotationManager.
bitmapFromDrawableRes(
this#AbsaLocationActivity,
R.drawable.red_marker
)?.let {
val annotationApi = mapView?.annotations
val pointAnnotationManager = annotationApi?.createPointAnnotationManager()
// Set options for the resulting symbol layer.
val pointAnnotationOptions: PointAnnotationOptions = PointAnnotationOptions()
// Define a geographic coordinate.
.withPoint(Point.fromLngLat(-15.39, 28.31))
// Specify the bitmap you assigned to the point annotation
// The bitmap will be added to map style automatically.
.withIconImage(it)
// Add the resulting pointAnnotation to the map.
pointAnnotationManager?.create(pointAnnotationOptions)
}
}
private fun bitmapFromDrawableRes(context: Context, #DrawableRes resourceId: Int) =
convertDrawableToBitmap(AppCompatResources.getDrawable(context, resourceId))
private fun convertDrawableToBitmap(sourceDrawable: Drawable?): Bitmap? {
if (sourceDrawable == null) {
return null
}
return if (sourceDrawable is BitmapDrawable) {
sourceDrawable.bitmap
} else {
// copying drawable object to not manipulate on the same reference
val constantState = sourceDrawable.constantState ?: return null
val drawable = constantState.newDrawable().mutate()
val bitmap: Bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth, drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
bitmap
}
}
}

Pixel art creator: loading saved pixel data from Room causes strange Canvas bug/glitch

Note: my question is very specific, apologies if the title isn't clear enough as to what the problem is.
I'm creating a pixel art editor application using Canvas, and the pixel art data is saved into a Room database.
Here's the canvas code:
package com.realtomjoney.pyxlmoose.customviews
import android.content.Context
import android.graphics.*
import android.util.Log
import android.view.MotionEvent
import android.view.View
import androidx.lifecycle.LifecycleOwner
import com.realtomjoney.pyxlmoose.activities.canvas.*
import com.realtomjoney.pyxlmoose.converters.JsonConverter
import com.realtomjoney.pyxlmoose.database.AppData
import com.realtomjoney.pyxlmoose.listeners.CanvasFragmentListener
import com.realtomjoney.pyxlmoose.models.Pixel
import kotlin.math.sqrt
class MyCanvasView(context: Context, val spanCount: Double) : View(context) {
lateinit var extraCanvas: Canvas
lateinit var extraBitmap: Bitmap
val rectangles = mutableMapOf<RectF, Paint?>()
private lateinit var caller: CanvasFragmentListener
private var thisWidth: Int = 0
private var scale: Double = 0.0
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
thisWidth = w
caller = context as CanvasFragmentListener
if (::extraBitmap.isInitialized) extraBitmap.recycle()
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
extraCanvas = Canvas(extraBitmap)
scale = (w / spanCount)
for (i in 0 until spanCount.toInt()) {
for (i_2 in 0 until spanCount.toInt()) {
val rect = RectF((i * scale).toFloat(), (i_2 * scale).toFloat(), (i * scale).toFloat() + scale.toFloat(), (i_2 * scale).toFloat() + scale.toFloat())
rectangles[rect] = null
extraCanvas.drawRect(rect, Paint().apply { style = Paint.Style.FILL; color = Color.WHITE })
}
}
}
private fun drawRectAt(x: Float, y: Float) {
for (rect in rectangles.keys) {
if (rect.contains(x, y)) {
caller.onPixelTapped(this, rect)
invalidate()
}
}
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_MOVE -> drawRectAt(event.x, event.y)
MotionEvent.ACTION_DOWN -> drawRectAt(event.x, event.y)
}
return true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
}
fun saveData(): List<Pixel> {
val data = mutableListOf<Pixel>()
for (pair in rectangles) {
data.add(Pixel(pair.value?.color))
}
return data
}
fun loadData(context: LifecycleOwner, index: Int) {
AppData.db.pixelArtCreationsDao().getAllPixelArtCreations().observe(context, {
currentPixelArtObj = it[index]
val localPixelData = JsonConverter.convertJsonStringToPixelList(currentPixelArtObj.pixelData)
var index = 0
for (i in 0 until sqrt(localPixelData.size.toDouble()).toInt()) {
for (i_2 in 0 until sqrt(localPixelData.size.toDouble()).toInt()) {
val rect = RectF((i * scale).toFloat(), (i_2 * scale).toFloat(), (i * scale).toFloat() + scale.toFloat(), (i_2 * scale).toFloat() + scale.toFloat())
rectangles[rect] = null
extraCanvas.drawRect(rect, Paint().apply { style = Paint.Style.FILL; isAntiAlias = false; color = localPixelData[index].pixelColor ?: Color.WHITE })
rectangles[rectangles.keys.toList()[index]] = Paint().apply { style = Paint.Style.FILL; isAntiAlias = false; color = localPixelData[index].pixelColor ?: Color.WHITE }
index++
}
}
})
}
}
Here's an example of how a 10 by 10 canvas may look like:
The pixel data is saved into a Room database as a Json String, and whenever we want to access this data we convert the Json String back to a List<Pixel>, et cetera:
Dao:
#Dao
interface PixelArtCreationsDao {
#Insert
suspend fun insertPixelArt(pixelArt: PixelArt)
#Query("SELECT * FROM PixelArt ")
fun getAllPixelArtCreations(): LiveData<List<PixelArt>>
#Query("DELETE FROM PixelArt WHERE objId=:pixelArtId")
fun deletePixelArtCreation(pixelArtId: Int)
#Query("UPDATE PixelArt SET item_bitmap=:bitmap WHERE objId=:id_t")
fun updatePixelArtCreationBitmap(bitmap: String, id_t: Int): Int
#Query("UPDATE PixelArt SET item_pixel_data=:pixelData WHERE objId=:id_t")
fun updatePixelArtCreationPixelData(pixelData: String, id_t: Int): Int
#Query("UPDATE PixelArt SET item_favourited=:favorited WHERE objId=:id_t")
fun updatePixelArtCreationFavorited(favorited: Boolean, id_t: Int): Int
}
PixelArt database:
#Database(entities = [PixelArt::class], version = 1)
abstract class PixelArtDatabase: RoomDatabase() {
abstract fun pixelArtCreationsDao(): PixelArtCreationsDao
companion object {
private var instance: PixelArtDatabase? = null
fun getDatabase(context: Context): PixelArtDatabase {
if (instance == null) {
synchronized(PixelArtDatabase::class) {
if (instance == null) instance = Room.databaseBuilder(context.applicationContext, PixelArtDatabase::class.java, AppData.dbFileName).allowMainThreadQueries().build()
}
}
return instance!!
}
}
}
AppData:
class AppData {
companion object {
var dbFileName = "pixel_art_db"
lateinit var db: PixelArtDatabase
}
}
Model:
#Entity
data class PixelArt(
#ColumnInfo(name = "item_bitmap") var bitmap: String,
#ColumnInfo(name = "item_title") var title: String,
#ColumnInfo(name = "item_pixel_data") var pixelData: String,
#ColumnInfo(name = "item_favourited") var favourited: Boolean,
#ColumnInfo(name = "item_date_created") var dateCreated: String = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(LocalDateTime.now())) {
#PrimaryKey(autoGenerate = true) var objId = 0
}
Now, say we have two projects like so with two different spanCount values:
Once we click on the first item, the following occurs:
For some reason it's setting the grid size to be equal to that of the second item, and I'm really trying to understand why this is the case. I've tried for a couple of hours to fix this weird glitch and have had no luck doing so.
But, for some reason when we go to our second item it renders properly:
If we create a new 80 x 80 canvas and then go back to the second creation it will render like so:
I'm assuming that it's setting the spanCount to that of the latest item in the database, but I'm unsure why this is happening.
I suspect it has something to do with the code that takes the List<Pixel> and draws it onscreen:
fun loadData(context: LifecycleOwner, index: Int) {
AppData.db.pixelArtCreationsDao().getAllPixelArtCreations().observe(context, {
currentPixelArtObj = it[index]
val localPixelData = JsonConverter.convertJsonStringToPixelList(currentPixelArtObj.pixelData)
var index = 0
for (i in 0 until sqrt(localPixelData.size.toDouble()).toInt()) {
for (i_2 in 0 until sqrt(localPixelData.size.toDouble()).toInt()) {
val rect = RectF((i * scale).toFloat(), (i_2 * scale).toFloat(), (i * scale).toFloat() + scale.toFloat(), (i_2 * scale).toFloat() + scale.toFloat())
rectangles[rect] = null
extraCanvas.drawRect(rect, Paint().apply { style = Paint.Style.FILL; isAntiAlias = false; color = localPixelData[index].pixelColor ?: Color.WHITE })
rectangles[rectangles.keys.toList()[index]] = Paint().apply { style = Paint.Style.FILL; isAntiAlias = false; color = localPixelData[index].pixelColor ?: Color.WHITE }
index++
}
}
})
}
Although I'm not entirely sure where the source of the bug is coming from because it seems I'm doing everything right. It's honestly been a brainfuck trying to fix this lol
Any help would be appreciated to fix this annoying glitch so I can finish my pixel art editor app.
This bug was fixed by calling invalidate() on the Fragment's Canvas property after the user taps the back button. It took me a couple of days to get to fix this, so I'm posting an answer here in case someone has a similar bug.
fun CanvasActivity.extendedOnBackPressed() {
canvasFragmentInstance.myCanvasViewInstance.invalidate()
startActivity(Intent(context, MainActivity::class.java))
}

Memory leak in Android Screenshot mechanism

I'm developing an Android app that uses screenshot mechanism and then sends the captured data over network in infinite loop. After some time of work the system kills my app without any exception. I made an experiment where the whole app was cut off besides the screen shooter and the problem is still here. Android profiler does not show any issues but I can see very large AbstractList$Itr and byte[] as on image:
If I increase the RAM then the app lives longer. I can't find the leak for two weaks...
Full source code of ScreenShooter class:
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.PixelFormat
import android.graphics.Point
import android.hardware.display.VirtualDisplay
import android.media.ImageReader
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.os.Handler
import android.os.HandlerThread
import android.os.Process
import android.view.Display
import android.view.WindowManager
import my_module.service.network.Networking
import java.io.ByteArrayOutputStream
import java.util.concurrent.locks.ReentrantLock
class ScreenShooter(private val network: Networking, private val context: Context): ImageReader.OnImageAvailableListener {
private val handlerThread: HandlerThread
private val handler: Handler
private var imageReader: ImageReader? = null
private var virtualDisplay: VirtualDisplay? = null
private var projection: MediaProjection? = null
private val mediaProjectionManager: MediaProjectionManager
private var latestBitmap: Bitmap? = null
private val byteStream = ByteArrayOutputStream()
private val mut: ReentrantLock = ReentrantLock()
private var width: Int
private var height: Int
private val TAG = ScreenShooter::class.java.simpleName
private val DELAY = 1000L
init {
handlerThread = HandlerThread(
javaClass.simpleName,
Process.THREAD_PRIORITY_BACKGROUND
).apply { start() }
handler = Handler(handlerThread.looper)
val windowManager = (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager)
val display: Display = windowManager.defaultDisplay
val size = Point()
display.getRealSize(size)
var width = size.x
var height = size.y
while (width * height > 2 shl 19) {
width = (width * 0.9).toInt()
height = (height * 0.9).toInt()
}
this.width = width
this.height = height
mediaProjectionManager = context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
}
override fun onImageAvailable(reader: ImageReader?) {
mut.lock()
val image = imageReader?.acquireLatestImage() ?: return
var buffer = image.planes[0].buffer
buffer.rewind()
latestBitmap?.copyPixelsFromBuffer(buffer)
latestBitmap?.compress(Bitmap.CompressFormat.JPEG, 80, byteStream)
network.sendScreen(byteStream.toByteArray())
buffer.clear()
buffer = null
image.close()
byteStream.flush()
byteStream.reset()
byteStream.close()
mut.unlock()
Thread.sleep(DELAY)
}
fun startCapture(resultCode: Int, resultData: Intent) {
latestBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
imageReader = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) {
ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2)
} else {
ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)
}
imageReader?.setOnImageAvailableListener(this, handler)
projection = mediaProjectionManager.getMediaProjection(resultCode, resultData)
virtualDisplay = projection!!.createVirtualDisplay(
"shooter",
width,
height,
context.resources.displayMetrics.densityDpi,
VIRT_DISPLAY_FLAGS,
imageReader?.surface,
null,
handler
)
projection?.registerCallback(projectionCallback, handler)
}
fun stopCapture() {
mut.lock()
imageReader?.setOnImageAvailableListener(null, null)
imageReader?.acquireLatestImage()?.close()
imageReader = null
projection?.unregisterCallback(projectionCallback)
projection?.stop()
virtualDisplay?.release()
imageReader?.close()
latestBitmap?.recycle()
mut.unlock()
}
private val projectionCallback = object : MediaProjection.Callback() {
override fun onStop() {
virtualDisplay?.release()
}
}
}
Methods startCapture() and stopCapture() are called from my background service in mainthread. network.sendScreen(byteStream.toByteArray()) just pushes the byte array (screenshot) to okhttp websocket queue:
fun sendScreen(bytes: ByteArray) {
val timestamp = System.currentTimeMillis()
val mss = Base64
.encodeToString(bytes, Base64.DEFAULT)
.replace("\n", "")
val message = """{ "data": {"timestamp":"$timestamp", "image":"$mss"} }"""
.trimMargin()
ws?.send(message.trimIndent())
}
Also I often get messages like Background concurrent copying GC freed but I got them without screenshooter with only network part and everything is Ok. I see no errors in logcat, no leaks in profiler.
I tried to recreate Screenshooter object in my service, but the app started to crash more often:
val t = HandlerThread(javaClass.simpleName).apply { start() }
Handler(t.looper).post {
while (true) {
Thread.sleep(10 * 1000L)
Log.d(TAG, "recreating the shooter")
Handler(mainLooper).post {
shooter!!.stopCapture()
shooter = null
shooter = ScreenShooter(network!!, applicationContext)
shooter!!.startCapture(resultCode, resultData!!)
}
}
}
However I supposed that this method would drop the previous created object out of GC Root and all its memory.
I've already no forces to search the leak. Thank you in advance.

Android Animated Carousel TextView within RecyclerView width wider than screen text gets clipped off. Even when animated into view

I am building a stock ticker type carousel that has market prices running along the screen as shown. The 'CRC' Ticker has only the partial view showing, it clips off at the edge of the parent which is the width of the device even when it is animated into view. I want it to have a width large enough hold all the children that goes past the device width.
Here if the Carousel layout xml:
<?xml version="1.0" encoding="utf-8"?>
<com.android.forexwatch.views.timepanel.CarouselView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/carouselLinear"
android:layout_width="match_parent"
android:layout_height="30dp"
android:orientation="horizontal"
android:singleLine="true"
android:animateLayoutChanges="true"
/>
Here is the class:
package com.android.forexwatch.views.timepanel
import android.content.Context
import android.util.AttributeSet
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.widget.RelativeLayout
import androidx.appcompat.widget.LinearLayoutCompat
import com.android.forexwatch.model.Index
import com.android.forexwatch.R
import com.android.forexwatch.model.CityInfoDetailed
import com.github.ybq.android.spinkit.SpinKitView
class CarouselView(context: Context, attrs: AttributeSet): LinearLayoutCompat(context, attrs) {
companion object {
private const val IndexWidth = 600F
}
private var contextThemeWrapper: ContextThemeWrapper? = null
init {
contextThemeWrapper = ContextThemeWrapper(context.applicationContext, R.style.Theme_ForexWatch)
}
fun setupView ( cityInfo: CityInfoDetailed?, isMirror: Boolean = false) {
createPriceTickerViews(cityInfo, isMirror)
}
private fun createPriceTickerViews(cityInfo: CityInfoDetailed?, isMirror: Boolean = false) {
destroy()
removeAllViews()
var calculatedWidth = 0
cityInfo?.indexes?.forEachIndexed {
index, element ->
if (isMirror) index.plus(cityInfo?.indexes.count())
val loader: SpinKitView = LayoutInflater.from(contextThemeWrapper).inflate(R.layout.preloader, null) as SpinKitView
val priceTicker: PriceTicker = LayoutInflater.from(contextThemeWrapper).inflate(R.layout.price_ticker, null) as PriceTicker
addView(priceTicker)
addView(loader)
// priceTicker.x = IndexWidth * index
calculatedWidth.plus(IndexWidth)
priceTicker.initialize(element, cityInfo, loader)
}
layoutParams.width = calculatedWidth
}
private fun destroy() {
for (idx in 0 .. this.childCount) {
val view = getChildAt(idx)
if (view is PriceTicker) {
view.destroy()
}
}
}
}
Here is the parent class:
package com.android.forexwatch.views.timepanel
import android.animation.ValueAnimator
import android.graphics.Color
import android.view.View
import android.view.ViewGroup
import android.view.animation.LinearInterpolator
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.android.forexwatch.R
import com.android.forexwatch.adapters.TimePanelRecyclerViewAdapter
import com.android.forexwatch.events.TimeEvent
import com.android.forexwatch.model.CityInfoDetailed
import com.android.forexwatch.model.TimeObject
import com.android.forexwatch.utils.TimeKeeper
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
class TimePanelView(view: View, container: ViewGroup, timePanelAdapter: TimePanelRecyclerViewAdapter) : RecyclerView.ViewHolder(view) {
companion object {
private const val OffsetTime: Long = 15 * 60
private const val FrameRate: Long = 32
}
private var fxTime: TextView? = null
private var stockTime: TextView? = null
private var city: TextView? = null
private var currentTime: TextView? = null
private var fxTimeLabel: TextView? = null
private var stockTimeLabel: TextView? = null
private var carouselView: CarouselView? = null
private var carouselViewMirror: CarouselView? = null
private var parentContainer: ViewGroup? = null
private var adapter: TimePanelRecyclerViewAdapter? = null
private var cityInfo: CityInfoDetailed? = null
private var view: View? = null
init {
adapter = timePanelAdapter
this.view = view
this.parentContainer = container
EventBus.getDefault().register(this)
}
fun setupView(cityInfo: CityInfoDetailed?) {
this.cityInfo = cityInfo
city = view?.findViewById(R.id.city)
stockTime = view?.findViewById(R.id.stockTime)
fxTime = view?.findViewById(R.id.fxTime)
fxTimeLabel = view?.findViewById(R.id.fxTimeLabel)
stockTimeLabel = view?.findViewById(R.id.stockTimeLabel)
currentTime = view?.findViewById(R.id.currentTime)
carouselView = view?.findViewById(R.id.carouselLinear)
carouselViewMirror = view?.findViewById(R.id.carouselLinearMirror)
city?.text = cityInfo?.cityName
createCarousel()
}
#Subscribe
fun update(event: TimeEvent.Interval?) {
updateCurrentTime()
updateFxTime()
updateStockTime()
}
private fun createCarousel() {
carouselView?.setupView(cityInfo)
carouselViewMirror?.setupView(cityInfo, true)
carouselView?.bringToFront()
carouselViewMirror?.bringToFront()
animateCarousel()
}
private fun updateCurrentTime() {
val timezone: String? = cityInfo?.timeZone
currentTime?.text = TimeKeeper.getCurrentTimeWithTimezone(timezone, "EEE' 'HH:mm:ss")
}
private fun updateFxTime() {
updateTime(fxTime, fxTimeLabel, cityInfo?.forexOpenTimes, cityInfo?.forexCloseTimes, cityInfo?.timeZone, "FX", Companion.OffsetTime * 8)
}
private fun updateStockTime() {
updateTime(stockTime, stockTimeLabel, cityInfo?.stockOpenTime, cityInfo?.stockCloseTime, cityInfo?.timeZone, cityInfo?.stockExchangeName, Companion.OffsetTime)
}
private fun updateTime(timeView: TextView?, timeLabel: TextView?, open: TimeObject?, close: TimeObject?, timezone: String?, type: String?, timeOffset: Long) {
if (TimeKeeper.isMarketOpen(open, close, timezone)) {
timeView?.text = TimeKeeper.getTimeDifference(close, close, timezone, true)
displayMarketColors(timeView, timeOffset, close, timezone, Color.GREEN, true)
timeLabel?.text = "To " + type + " Close"
} else {
timeView?.text = TimeKeeper.getTimeDifference(open, close, timezone, false)
displayMarketColors(timeView, timeOffset, open, timezone, Color.RED, false)
timeLabel?.text = "To " + type + " Open"
}
}
private fun displayMarketColors(timeView: TextView?, timeOffset: Long, time: TimeObject?, timezone: String?, outRangeColor: Int, isMarketOpen: Boolean?) {
val color = if (TimeKeeper.isTimeWithinRange(timeOffset, time, timezone, isMarketOpen)) Color.parseColor("#FF7F00") else outRangeColor
timeView?.setTextColor(color)
}
private fun animateCarousel() {
if (cityInfo?.indexes?.count() == 1) {
return
}
// carouselView?.x = carouselView?.x?.minus(3.0F)!!
/* CoroutineScope(Dispatchers.Main).launch {
delay(FrameRate)
animateCarousel()
}*/
val animator = ValueAnimator.ofFloat(0.0f, 1.0f)
animator.repeatCount = ValueAnimator.INFINITE
animator.interpolator = LinearInterpolator()
animator.duration = 18000L
animator.addUpdateListener { animation ->
val progress = animation.animatedValue as Float
val width: Int? = carouselView?.width
val translationX = -width?.times(progress)!!
carouselView?.translationX = translationX!!
carouselViewMirror?.translationX = translationX!! + width!!
}
animator.start()
}
}
The PriceTicker layout which extends AppCompatTextView
<com.android.forexwatch.views.timepanel.PriceTicker
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
class="com.android.forexwatch.views.timepanel.PriceTicker"
android:layout_width="240dp"
android:layout_height="match_parent"
android:textAlignment="center"
android:textSize="13sp"
android:singleLine="true"
android:background="#drawable/rectangle_shape"
app:layout_constraintBottom_toBottomOf="parent"
/>
I found the solution by invalidating and requestingLayout on the priceTicker TextView and the container. Also changed the width of the container and priceTicker to the desired width.
calculatedWidth = calculatedWidth.plus(IndexWidth).toInt()
priceTicker.initialize(index, cityInfo, loader)
priceTicker.width = IndexWidth.toInt()
resetLayout(priceTicker)
}
layoutParams.width = (calculatedWidth)
resetLayout(thisContainer)
}
}
private fun destroy() {
for (idx in 0 .. this.childCount) {
val view = getChildAt(idx)
if (view is PriceTicker) {
view.destroy()
}
}
}
private fun resetLayout(view: View?) {
view?.invalidate()
view?.requestLayout()
}

Converting a Bitmap to a WebRTC VideoFrame

I'm working on a WebRTC based app for Android using the native implementation (org.webrtc:google-webrtc:1.0.24064), and I need to send a series of bitmaps along with the camera stream.
From what I understood, I can derive from org.webrtc.VideoCapturer and do my rendering in a separate thread, and send video frames to the observer; however it expects them to be YUV420 and I'm not sure I'm doing the correct conversion.
This is what I currently have: CustomCapturer.java
Are there any examples I can look at for doing this kind of things? Thanks.
YuvConverter yuvConverter = new YuvConverter();
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
TextureBufferImpl buffer = new TextureBufferImpl(bitmap.getWidth(), bitmap.getHeight(), VideoFrame.TextureBuffer.Type.RGB, textures[0], new Matrix(), textureHelper.getHandler(), yuvConverter, null);
VideoFrame.I420Buffer i420Buf = yuvConverter.convert(buffer);
VideoFrame CONVERTED_FRAME = new VideoFrame(i420Buf, 180, videoFrame.getTimestampNs()) ;
I've tried rendering it manually with GL as in Yang's answer, but that ended up with some tearing and framerate issues when dealing with a stream of images.
Instead, I've found that the SurfaceTextureHelper class helps simplify things quite a bit, as you can also use regular canvas drawing to render the bitmap into a VideoFrame. I'm guessing it still uses GL under the hood, as the performance was otherwise comparable. Here's an example VideoCapturer that takes in arbitrary bitmaps and outputs the captured frames to its observer:
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Matrix
import android.graphics.Paint
import android.os.Build
import android.view.Surface
import org.webrtc.CapturerObserver
import org.webrtc.SurfaceTextureHelper
import org.webrtc.VideoCapturer
/**
* A [VideoCapturer] that can be manually driven by passing in [Bitmap].
*
* Once [startCapture] is called, call [pushBitmap] to render images as video frames.
*/
open class BitmapFrameCapturer : VideoCapturer {
private var surfaceTextureHelper: SurfaceTextureHelper? = null
private var capturerObserver: CapturerObserver? = null
private var disposed = false
private var rotation = 0
private var width = 0
private var height = 0
private val stateLock = Any()
private var surface: Surface? = null
override fun initialize(
surfaceTextureHelper: SurfaceTextureHelper,
context: Context,
observer: CapturerObserver,
) {
synchronized(stateLock) {
this.surfaceTextureHelper = surfaceTextureHelper
this.capturerObserver = observer
surface = Surface(surfaceTextureHelper.surfaceTexture)
}
}
private fun checkNotDisposed() {
check(!disposed) { "Capturer is disposed." }
}
override fun startCapture(width: Int, height: Int, framerate: Int) {
synchronized(stateLock) {
checkNotDisposed()
checkNotNull(surfaceTextureHelper) { "BitmapFrameCapturer must be initialized before calling startCapture." }
capturerObserver?.onCapturerStarted(true)
surfaceTextureHelper?.startListening { frame -> capturerObserver?.onFrameCaptured(frame) }
}
}
override fun stopCapture() {
synchronized(stateLock) {
surfaceTextureHelper?.stopListening()
capturerObserver?.onCapturerStopped()
}
}
override fun changeCaptureFormat(width: Int, height: Int, framerate: Int) {
// Do nothing.
// These attributes are driven by the bitmaps fed in.
}
override fun dispose() {
synchronized(stateLock) {
if (disposed) {
return
}
stopCapture()
surface?.release()
disposed = true
}
}
override fun isScreencast(): Boolean = false
fun pushBitmap(bitmap: Bitmap, rotationDegrees: Int) {
synchronized(stateLock) {
if (disposed) {
return
}
checkNotNull(surfaceTextureHelper)
checkNotNull(surface)
if (this.rotation != rotationDegrees) {
surfaceTextureHelper?.setFrameRotation(rotationDegrees)
this.rotation = rotationDegrees
}
if (this.width != bitmap.width || this.height != bitmap.height) {
surfaceTextureHelper?.setTextureSize(bitmap.width, bitmap.height)
this.width = bitmap.width
this.height = bitmap.height
}
surfaceTextureHelper?.handler?.post {
val canvas = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
surface?.lockHardwareCanvas()
} else {
surface?.lockCanvas(null)
}
if (canvas != null) {
canvas.drawBitmap(bitmap, Matrix(), Paint())
surface?.unlockCanvasAndPost(canvas)
}
}
}
}
}
https://github.com/livekit/client-sdk-android/blob/c1e207c30fce9499a534e13c63a59f26215f0af4/livekit-android-sdk/src/main/java/io/livekit/android/room/track/video/BitmapFrameCapturer.kt

Categories

Resources