How to have similar mechanism of center-crop on ExoPlayer's PlayerView , but not on the center? How to have similar mechanism of center-crop on ExoPlayer's PlayerView , but not on the center? android android

How to have similar mechanism of center-crop on ExoPlayer's PlayerView , but not on the center?


The question is how to manipulate an image like ImageView.ScaleType.CENTER_CROP but to shift the focus from the center to another location that is 20% from the top of the image. First, let's look at what CENTER_CROP does:

From the documentation:

CENTER_CROP

Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width and height) of the image will be equal to or larger than the corresponding dimension of the view (minus padding). The image is then centered in the view. From XML, use this syntax: android:scaleType="centerCrop".

In other words, scale the image without distortion such that either the width or height of the image (or both width and height) fit within the view so that the view is completely filled with the image (no gaps.)

Another way to think of this is that the center of the image is "pinned" to the center of the view. The image is then scaled to meet the criteria above.

In the following video, the white lines mark the center of the image; the red lines mark the center of the view. The scale type is CENTER_CROP. Notice how the center points of the image and the view coincide. As the view changes size, these two points continue to overlap and always appear at the center of the view regardless of the view size.

enter image description here

So, what does it mean to have center crop-like behavior at a different location such as 20% from the top? Like center crop, we can specify that the point that is 20% from the top of the image and the point that 20% from the top of the view will be "pinned" like the 50% point is "pinned" in center crop. The horizontal location of this point remains at 50% of the image and view. The image can now be scaled to satisfy the other conditions of center crop which specify that either the width and/or height of the image will fit the view with no gaps. (Size of view is understood to be the view size less padding.)

Here is a short video of this 20% crop behavior. In this video, the white lines show the middle of the image, the red lines show the pinned point in the view and the blue line that shows behind the horizontal red line identifies 20% from the top of the image. (Demo project is on GitHub.

enter image description here

Here is the result showing the full image that was supplied and the video in a square frame that transition from the still image. .

enter image description here

MainActivity.kt
prepareMatrix() is the method that does the work to determine how to scale/crop the image. There is some additional work to be done with the video since it appears that the video is made to fit the TextureViewas a scale type "FIT_XY" when it is assigned to the TextureView. Because of this scaling, the media size must be restored before prepareMatrix() is called for the video

class MainActivity : AppCompatActivity() {    private val imageResId = R.drawable.test    private val videoResId = R.raw.test    private var player: SimpleExoPlayer? = null    private val mFocalPoint = PointF(0.5f, 0.2f)    override fun onCreate(savedInstanceState: Bundle?) {        window.setBackgroundDrawable(ColorDrawable(0xff000000.toInt()))        super.onCreate(savedInstanceState)        if (cache == null) {            cache = SimpleCache(File(cacheDir, "media"), LeastRecentlyUsedCacheEvictor(MAX_PREVIEW_CACHE_SIZE_IN_BYTES))        }        setContentView(R.layout.activity_main)        //        imageView.visibility = View.INVISIBLE        imageView.setImageResource(imageResId)        imageView.doOnPreDraw {            imageView.scaleType = ImageView.ScaleType.MATRIX            val imageWidth: Float = ContextCompat.getDrawable(this, imageResId)!!.intrinsicWidth.toFloat()            val imageHeight: Float = ContextCompat.getDrawable(this, imageResId)!!.intrinsicHeight.toFloat()            imageView.imageMatrix = prepareMatrix(imageView, imageWidth, imageHeight, mFocalPoint, Matrix())            val b = BitmapFactory.decodeResource(resources, imageResId)            val d = BitmapDrawable(resources, b.copy(Bitmap.Config.ARGB_8888, true))            val c = Canvas(d.bitmap)            val p = Paint()            p.color = resources.getColor(android.R.color.holo_red_dark)            p.style = Paint.Style.STROKE            val strokeWidth = 10            p.strokeWidth = strokeWidth.toFloat()            // Horizontal line            c.drawLine(0f, imageHeight * mFocalPoint.y, imageWidth, imageHeight * mFocalPoint.y, p)            // Vertical line            c.drawLine(imageWidth * mFocalPoint.x, 0f, imageWidth * mFocalPoint.x, imageHeight, p)            // Line in horizontal and vertical center            p.color = resources.getColor(android.R.color.white)            c.drawLine(imageWidth / 2, 0f, imageWidth / 2, imageHeight, p)            c.drawLine(0f, imageHeight / 2, imageWidth, imageHeight / 2, p)            imageView.setImageBitmap(d.bitmap)            imageViewFull.setImageBitmap(d.bitmap)        }    }    fun startPlay(view: View) {        playVideo()    }    private fun getViewWidth(view: View): Float {        return (view.width - view.paddingStart - view.paddingEnd).toFloat()    }    private fun getViewHeight(view: View): Float {        return (view.height - view.paddingTop - view.paddingBottom).toFloat()    }    private fun prepareMatrix(targetView: View, mediaWidth: Float, mediaHeight: Float,                              focalPoint: PointF, matrix: Matrix): Matrix {        if (targetView.visibility != View.VISIBLE) {            return matrix        }        val viewHeight = getViewHeight(targetView)        val viewWidth = getViewWidth(targetView)        val scaleFactorY = viewHeight / mediaHeight        val scaleFactor: Float        val px: Float        val py: Float        if (mediaWidth * scaleFactorY >= viewWidth) {            // Fit height            scaleFactor = scaleFactorY            px = -(mediaWidth * scaleFactor - viewWidth) * focalPoint.x / (1 - scaleFactor)            py = 0f        } else {            // Fit width            scaleFactor = viewWidth / mediaWidth            px = 0f            py = -(mediaHeight * scaleFactor - viewHeight) * focalPoint.y / (1 - scaleFactor)        }        matrix.postScale(scaleFactor, scaleFactor, px, py)        return matrix    }    private fun playVideo() {        player = ExoPlayerFactory.newSimpleInstance(this@MainActivity, DefaultTrackSelector())        player!!.setVideoTextureView(textureView)        player!!.addVideoListener(object : VideoListener {            override fun onVideoSizeChanged(width: Int, height: Int, unappliedRotationDegrees: Int, pixelWidthHeightRatio: Float) {                super.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio)                val matrix = Matrix()                // Restore true media size for further manipulation.                matrix.setScale(width / getViewWidth(textureView), height / getViewHeight(textureView))                textureView.setTransform(prepareMatrix(textureView, width.toFloat(), height.toFloat(), mFocalPoint, matrix))            }            override fun onRenderedFirstFrame() {                Log.d("AppLog", "onRenderedFirstFrame")                player!!.removeVideoListener(this)                imageView.animate().alpha(0f).setDuration(2000).start()                imageView.visibility = View.INVISIBLE            }        })        player!!.volume = 0f        player!!.repeatMode = Player.REPEAT_MODE_ALL        player!!.playRawVideo(this, videoResId)        player!!.playWhenReady = true        //        player!!.playVideoFromUrl(this, "https://sample-videos.com/video123/mkv/240/big_buck_bunny_240p_20mb.mkv", cache!!)        //        player!!.playVideoFromUrl(this, "https://sample-videos.com/video123/mkv/720/big_buck_bunny_720p_1mb.mkv", cache!!)        //        player!!.playVideoFromUrl(this@MainActivity, "https://sample-videos.com/video123/mkv/720/big_buck_bunny_720p_1mb.mkv")    }    override fun onStop() {        super.onStop()        if (player != null) {            player!!.setVideoTextureView(null)            //        playerView.player = null            player!!.release()            player = null        }    }    companion object {        const val MAX_PREVIEW_CACHE_SIZE_IN_BYTES = 20L * 1024L * 1024L        var cache: com.google.android.exoplayer2.upstream.cache.Cache? = null        @JvmStatic        fun getUserAgent(context: Context): String {            val packageManager = context.packageManager            val info = packageManager.getPackageInfo(context.packageName, 0)            val appName = info.applicationInfo.loadLabel(packageManager).toString()            return Util.getUserAgent(context, appName)        }    }    fun SimpleExoPlayer.playRawVideo(context: Context, @RawRes rawVideoRes: Int) {        val dataSpec = DataSpec(RawResourceDataSource.buildRawResourceUri(rawVideoRes))        val rawResourceDataSource = RawResourceDataSource(context)        rawResourceDataSource.open(dataSpec)        val factory: DataSource.Factory = DataSource.Factory { rawResourceDataSource }        prepare(LoopingMediaSource(ExtractorMediaSource.Factory(factory).createMediaSource(rawResourceDataSource.uri)))    }    fun SimpleExoPlayer.playVideoFromUrl(context: Context, url: String, cache: Cache? = null) = playVideoFromUri(context, Uri.parse(url), cache)    fun SimpleExoPlayer.playVideoFile(context: Context, file: File) = playVideoFromUri(context, Uri.fromFile(file))    fun SimpleExoPlayer.playVideoFromUri(context: Context, uri: Uri, cache: Cache? = null) {        val factory = if (cache != null)            CacheDataSourceFactory(cache, DefaultHttpDataSourceFactory(getUserAgent(context)))        else            DefaultDataSourceFactory(context, MainActivity.getUserAgent(context))        val mediaSource = ExtractorMediaSource.Factory(factory).createMediaSource(uri)        prepare(mediaSource)    }}


you can use app:resize_mode="zoom" in com.google.android.exoplayer2.ui.PlayerView


I had a similar problem and solved it by applying transformations on the TextureView whose Surface is used by ExoPlayer:

player.addVideoListener(object : VideoListener {    override fun onVideoSizeChanged(        videoWidth: Int,        videoHeight: Int,        unappliedRotationDegrees: Int,        pixelWidthHeightRatio: Float,    ) {        removeVideoListener(this)        val viewWidth: Int = textureView.width - textureView.paddingStart - textureView.paddingEnd        val viewHeight: Int = textureView.height - textureView.paddingTop - textureView.paddingBottom        if (videoWidth == viewWidth && videoHeight == viewHeight) {            return        }        val matrix = Matrix().apply {            // TextureView makes a best effort in fitting the video inside the View. The first transformation we apply is for reverting the fitting.            setScale(                videoWidth.toFloat() / viewWidth,                videoHeight.toFloat() / viewHeight,            )        }                // This algorithm is from ImageView's CENTER_CROP transformation        val offset = 0.5f // the center in CENTER_CROP but you probably want a different value here        val scale: Float        val dx: Float        val dy: Float        if (videoWidth * viewHeight > viewWidth * videoHeight) {            scale = viewHeight.toFloat() / videoHeight            dx = (viewWidth - videoWidth * scale) * offset            dy = 0f        } else {            scale = viewWidth.toFloat() / videoWidth            dx = 0f            dy = (viewHeight - videoHeight * scale) * offset        }        setTransform(matrix.apply {            postScale(scale, scale)            postTranslate(dx, dy)        })    }})player.setVideoTextureView(textureView)player.prepare(createMediaSource())

Note that unless you're using DefaultRenderersFactory you need to make sure that your video Renderer actually calls onVideoSizeChanged by for instance creating the factory like so:

val renderersFactory = RenderersFactory { handler, videoListener, _, _, _, _ ->        // Allows other renderers to be removed by R8        arrayOf(            MediaCodecVideoRenderer(                context,                MediaCodecSelector.DEFAULT,                DefaultRenderersFactory.DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS,                handler,                videoListener,                -1,            ),            MediaCodecAudioRenderer(context, MediaCodecSelector.DEFAULT),        )    }