【问题标题】:How to have similar mechanism of center-crop on ExoPlayer's PlayerView , but not on the center?如何在 ExoPlayer 的 PlayerView 上有类似的 center-crop 机制,但不在中心?
【发布时间】:2019-06-10 11:50:00
【问题描述】:

背景

我们录制用户脸部的视频,通常脸部位于视频的上半部分。

稍后我们希望观看视频,但PlayerView 的宽高比可能与视频不同,因此需要进行一些缩放和裁剪。

问题

我发现缩放PlayerView 以便它显示在它拥有的整个空间中但保持纵横比(当然,这将导致在需要时进行裁剪)的唯一方法是使用@ 987654336@。这是它如何与 center-crop 一起使用的示例:http://s000.tinyupload.com/?file_id=00574047057406286563。显示内容的视图具有相似的纵横比越多,需要的裁剪就越少。

但这仅适用于中心,这意味着它需要视频的 0.5x0.5 点,并从该点进行缩放。这会导致很多情况下丢失视频的重要内容。

例如,如果我们有一个纵向拍摄的视频,并且我们有一个正方形的 PlayerView 并且想要显示顶部区域,那么这就是可见的部分:

当然,如果内容本身是方形的,视图也是方形的,应该显示整个内容,不要裁剪。

我尝试过的

我尝试在 Internet、StackOverflow(此处)和 Github 上进行搜索,但找不到方法。我发现的唯一线索是关于 AspectRatioFrameLayout 和 AspectRatioTextureView,但我没有找到如何将它们用于此任务,如果可能的话。

有人告诉我 (here) 我应该使用普通的 TextureView ,并使用 SimpleExoPlayer.setVideoTextureView 将其直接提供给 SimpleExoPlayer。并使用TextureView.setTransform 对其进行特殊转换。

经过大量尝试最好使用的方法(并查看 video-crop repositorySuperImageView repositoryJCropImageView repository ,其中有 ImageView 和视频的缩放/裁剪示例),我发布了一个工作示例似乎可以正确显示视频,但我仍然不确定,因为我还使用了一个在它开始播放之前显示在其顶部的 ImageView(以获得更好的过渡而不是黑色内容)。

这是当前代码:

class MainActivity : AppCompatActivity() {
    private val imageResId = R.drawable.test
    private val videoResId = R.raw.test
    private val percentageY = 0.2f
    private var player: SimpleExoPlayer? = null


    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.imageMatrix = prepareMatrixForImageView(imageView, imageView.drawable.intrinsicWidth.toFloat(), imageView.drawable.intrinsicHeight.toFloat())
//            imageView.imageMatrix = prepareMatrix(imageView, imageView.drawable.intrinsicWidth.toFloat(), imageView.drawable.intrinsicHeight.toFloat())
//            imageView.visibility = View.VISIBLE
        }
    }

    override fun onStart() {
        super.onStart()
        playVideo()
    }

    private fun prepareMatrix(view: View, contentWidth: Float, contentHeight: Float): Matrix {
        var scaleX = 1.0f
        var scaleY = 1.0f
        val viewWidth = view.measuredWidth.toFloat()
        val viewHeight = view.measuredHeight.toFloat()
        Log.d("AppLog", "viewWidth $viewWidth viewHeight $viewHeight contentWidth:$contentWidth contentHeight:$contentHeight")
        if (contentWidth > viewWidth && contentHeight > viewHeight) {
            scaleX = contentWidth / viewWidth
            scaleY = contentHeight / viewHeight
        } else if (contentWidth < viewWidth && contentHeight < viewHeight) {
            scaleY = viewWidth / contentWidth
            scaleX = viewHeight / contentHeight
        } else if (viewWidth > contentWidth)
            scaleY = viewWidth / contentWidth / (viewHeight / contentHeight)
        else if (viewHeight > contentHeight)
            scaleX = viewHeight / contentHeight / (viewWidth / contentWidth)
        val matrix = Matrix()
        val pivotPercentageX = 0.5f
        val pivotPercentageY = percentageY

        matrix.setScale(scaleX, scaleY, viewWidth * pivotPercentageX, viewHeight * pivotPercentageY)
        return matrix
    }

    private fun prepareMatrixForVideo(view: View, contentWidth: Float, contentHeight: Float): Matrix {
        val msWidth = view.measuredWidth
        val msHeight = view.measuredHeight
        val matrix = Matrix()
        matrix.setScale(1f, (contentHeight / contentWidth) * (msWidth.toFloat() / msHeight), msWidth / 2f, percentageY * msHeight) /*,msWidth/2f,msHeight/2f*/
        return matrix
    }

    private fun prepareMatrixForImageView(view: View, contentWidth: Float, contentHeight: Float): Matrix {
        val dw = contentWidth
        val dh = contentHeight
        val msWidth = view.measuredWidth
        val msHeight = view.measuredHeight
//        Log.d("AppLog", "viewWidth $msWidth viewHeight $msHeight contentWidth:$contentWidth contentHeight:$contentHeight")
        val scalew = msWidth.toFloat() / dw
        val theoryh = (dh * scalew).toInt()
        val scaleh = msHeight.toFloat() / dh
        val theoryw = (dw * scaleh).toInt()
        val scale: Float
        var dx = 0
        var dy = 0
        if (scalew > scaleh) { // fit width
            scale = scalew
//            dy = ((msHeight - theoryh) * 0.0f + 0.5f).toInt() // + 0.5f for rounding
        } else {
            scale = scaleh
            dx = ((msWidth - theoryw) * 0.5f + 0.5f).toInt() // + 0.5f for rounding
        }
        dy = ((msHeight - theoryh) * percentageY + 0.5f).toInt() // + 0.5f for rounding
        val matrix = Matrix()
//        Log.d("AppLog", "scale:$scale dx:$dx dy:$dy")
        matrix.setScale(scale, scale)
        matrix.postTranslate(dx.toFloat(), dy.toFloat())
        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)
                Log.d("AppLog", "onVideoSizeChanged: $width $height")
                val videoWidth = if (unappliedRotationDegrees % 180 == 0) width else height
                val videoHeight = if (unappliedRotationDegrees % 180 == 0) height else width
                val matrix = prepareMatrixForVideo(textureView, videoWidth.toFloat(), videoHeight.toFloat())
                textureView.setTransform(matrix)
            }

            override fun onRenderedFirstFrame() {
                Log.d("AppLog", "onRenderedFirstFrame")
                player!!.removeVideoListener(this)
//                imageView.animate().alpha(0f).setDuration(5000).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()
        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)
    }
}

在了解当前情况之前,我在尝试此操作时遇到了各种问题,因此我已多次更新此问题。现在它甚至适用于我谈到的百分比,所以如果我愿意,我可以将其设置为视频顶部的 20%。但是,我仍然认为它很有可能出现问题,因为当我尝试将其设置为 50% 时,我注意到内容可能不适合整个 View。

我什至查看了 ImageView (here) 的源代码,了解如何使用 center-crop。当应用于 ImageView 时,它仍然可以作为 center-crop,但是当我在视频上使用相同的技术时,它给了我一个非常错误的结果。

问题

我的目标是同时显示 ImageView 和视频,以便从静态图像平滑过渡到视频。所有这一切,虽然两者都具有从顶部起 20% 的顶级作物(例如)。我已经发布了一个示例项目here 来试用它并与人们分享我的发现。

所以现在我的问题是为什么这似乎不适用于 imageView 和/或视频:

  1. 事实证明,我尝试过的所有矩阵创建都不适用于 ImageView 或视频。它到底有什么问题?我怎样才能改变它让它们看起来一样?例如,要从前 20% 进行规模种植?

  2. 我尝试对两者都使用精确矩阵,但似乎每个都需要不同的矩阵,即使两者具有完全相同的大小和内容大小。为什么我需要一个不同的矩阵?


编辑:在回答了这个问题后,我决定制作一个如何使用它的小示例(Github 存储库可用here):

import android.content.Context
import android.graphics.Matrix
import android.graphics.PointF
import android.net.Uri
import android.os.Bundle
import android.view.TextureView
import android.view.View
import androidx.annotation.RawRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.doOnPreDraw
import com.google.android.exoplayer2.ExoPlayerFactory
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.source.ExtractorMediaSource
import com.google.android.exoplayer2.source.LoopingMediaSource
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.upstream.*
import com.google.android.exoplayer2.upstream.cache.Cache
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
import com.google.android.exoplayer2.upstream.cache.SimpleCache
import com.google.android.exoplayer2.util.Util
import com.google.android.exoplayer2.video.VideoListener
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File

// https://stackoverflow.com/questions/54216273/how-to-have-similar-mechanism-of-center-crop-on-exoplayers-playerview-but-not
class MainActivity : AppCompatActivity() {
    companion object {
        private val FOCAL_POINT = PointF(0.5f, 0.2f)
        private const val IMAGE_RES_ID = R.drawable.test
        private const val VIDEO_RES_ID = R.raw.test
        private var cache: Cache? = null
        private const val MAX_PREVIEW_CACHE_SIZE_IN_BYTES = 20L * 1024L * 1024L

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

    private var player: SimpleExoPlayer? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        if (cache == null)
            cache = SimpleCache(File(cacheDir, "media"), LeastRecentlyUsedCacheEvictor(MAX_PREVIEW_CACHE_SIZE_IN_BYTES))
        //        imageView.visibility = View.INVISIBLE
        imageView.setImageResource(IMAGE_RES_ID)
    }

    private fun prepareMatrix(view: View, mediaWidth: Float, mediaHeight: Float, focalPoint: PointF): Matrix? {
        if (view.visibility == View.GONE)
            return null
        val viewHeight = (view.height - view.paddingTop - view.paddingBottom).toFloat()
        val viewWidth = (view.width - view.paddingStart - view.paddingEnd).toFloat()
        if (viewWidth <= 0 || viewHeight <= 0)
            return null
        val matrix = Matrix()
        if (view is TextureView)
        // Restore true media size for further manipulation.
            matrix.setScale(mediaWidth / viewWidth, mediaHeight / viewHeight)
        val scaleFactorY = viewHeight / mediaHeight
        val scaleFactor: Float
        var px = 0f
        var py = 0f
        if (mediaWidth * scaleFactorY >= viewWidth) {
            // Fit height
            scaleFactor = scaleFactorY
            px = -(mediaWidth * scaleFactor - viewWidth) * focalPoint.x / (1 - scaleFactor)
        } else {
            // Fit width
            scaleFactor = viewWidth / mediaWidth
            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(videoWidth: Int, videoHeight: Int, unappliedRotationDegrees: Int, pixelWidthHeightRatio: Float) {
                super.onVideoSizeChanged(videoWidth, videoHeight, unappliedRotationDegrees, pixelWidthHeightRatio)
                textureView.setTransform(prepareMatrix(textureView, videoWidth.toFloat(), videoHeight.toFloat(), FOCAL_POINT))
            }

            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, VIDEO_RES_ID)
        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 onStart() {
        super.onStart()
        imageView.doOnPreDraw {
            val imageWidth: Float = imageView.drawable.intrinsicWidth.toFloat()
            val imageHeight: Float = imageView.drawable.intrinsicHeight.toFloat()
            imageView.imageMatrix = prepareMatrix(imageView, imageWidth, imageHeight, FOCAL_POINT)
        }
        playVideo()
    }

    override fun onStop() {
        super.onStop()
        if (player != null) {
            player!!.setVideoTextureView(null)
            //        playerView.player = null
            player!!.release()
            player = null
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        if (!isChangingConfigurations)
            cache?.release()
    }

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

如果需要,这里有一个单独的 ImageView 解决方案:

class ScaleCropImageView(context: Context, attrs: AttributeSet?) : AppCompatImageView(context, attrs) {
    var focalPoint = PointF(0.5f, 0.5f)
        set(value) {
            field = value
            updateMatrix()
        }
    private val viewWidth: Float
        get() = (width - paddingLeft - paddingRight).toFloat()

    private val viewHeight: Float
        get() = (height - paddingTop - paddingBottom).toFloat()

    init {
        scaleType = ScaleType.MATRIX
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        updateMatrix()
    }

    override fun setImageDrawable(drawable: Drawable?) {
        super.setImageDrawable(drawable)
        updateMatrix()
    }

    @Suppress("MemberVisibilityCanBePrivate")
    fun updateMatrix() {
        if (scaleType != ImageView.ScaleType.MATRIX)
            return
        val dr = drawable ?: return
        imageMatrix = prepareMatrix(
                viewWidth, viewHeight,
                dr.intrinsicWidth.toFloat(), dr.intrinsicHeight.toFloat(), focalPoint, Matrix()
        )
    }

    private fun prepareMatrix(
            viewWidth: Float, viewHeight: Float, mediaWidth: Float, mediaHeight: Float,
            focalPoint: PointF, matrix: Matrix
    ): Matrix? {
        if (viewWidth <= 0 || viewHeight <= 0)
            return null
        var scaleFactor = viewHeight / mediaHeight
        if (mediaWidth * scaleFactor >= viewWidth) {
            // Fit height
            matrix.postScale(scaleFactor, scaleFactor, -(mediaWidth * scaleFactor - viewWidth) * focalPoint.x / (1 - scaleFactor), 0f)
        } else {
            // Fit width
            scaleFactor = viewWidth / mediaWidth
            matrix.postScale(scaleFactor, scaleFactor, 0f, -(mediaHeight * scaleFactor - viewHeight) * focalPoint.y / (1 - scaleFactor))
        }
        return matrix
    }
}

【问题讨论】:

  • @MartinZeitler 是的,我知道。我检查了错误的变量。我的意思是检查度数,出于某种原因检查了宽度和高度......不过,我对 ImageView 和视频容器有疑问。请,如果您知道为什么会发生这种情况,请告诉我。
  • 仅根据发布的图像,我怀疑这个主题是相关的:math.stackexchange.com/questions/180804/… ...其中视频纵横比和显示(或表面)纵横比都需要被考虑...为了得到一个看起来自然的结果。这些的某些组合可能需要大量裁剪 - 或有边界。还应该有一个计算最佳中心作物的公式。基本上它只是两个相互关联的矩形。
  • @MartinZeitler 但我在 ImageView 上使用了与视频完全相同的技术。两者都获得相同的视图宽度和高度以及函数内部的内容。
  • @MartinZeitler 我认为视频矩阵也不正确,而不仅仅是 ImageView。我希望我能尽快找到解决方案。

标签: android scale crop exoplayer


【解决方案1】:

我遇到了类似的问题,并通过对 ExoPlayer 使用的SurfaceTextureView 应用转换来解决它:


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

请注意,除非您使用 DefaultRenderersFactory,否则您需要确保您的视频 Renderer 实际调用 onVideoSizeChanged,例如像这样创建工厂:

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

【讨论】:

    【解决方案2】:

    您可以在 com.google.android.exoplayer2.ui.PlayerView 中使用 app:resize_mode="zoom"

    【讨论】:

    • zoom 可能会导致 CENTER_CROP 行为,但最初的问题是如何裁剪视频的另一部分中心。
    【解决方案3】:

    问题是如何操作像ImageView.ScaleType.CENTER_CROP 这样的图像,但要将焦点从中心转移到距离图像顶部 20% 的另一个位置。首先我们来看看CENTER_CROP做了什么:

    来自documentation

    CENTER_CROP

    均匀缩放图像(保持图像的纵横比),使图像的两个尺寸(宽度和高度)都等于或大于视图的相应尺寸(减去填充)。然后图像在视图中居中。在 XML 中,使用以下语法:android:scaleType="centerCrop"

    换句话说,在不失真的情况下缩放图像,使图像的宽度或高度(或宽度和高度)都适合视图,从而使视图完全被图像填充(没有间隙)。

    另一种思考方式是图像的中心被“固定”到视图的中心。然后缩放图像以满足上述标准。

    在下面的视频中,白线标记了图像的中心;红线标记视图的中心。比例类型为CENTER_CROP。注意图像和视图的中心点是如何重合的。随着视图大小的改变,这两个点会继续重叠并始终出现在视图的中心,而与视图大小无关。

    那么,在不同的位置(例如距顶部 20%)具有中心作物样行为是什么意思?与中心裁剪一样,我们可以指定距图像顶部 20% 的点以及距视图顶部 20% 的点将被“固定”,就像 50% 的点在中心裁剪中“固定”一样。该点的水平位置保持在图像和视图的 50% 处。现在可以缩放图像以满足中心裁剪的其他条件,这些条件指定图像的宽度和/或高度将适合视图而没有间隙。 (Size of view 被理解为 view size 减去 padding。)

    这里是这个 20% 裁剪行为的简短视频。在此视频中,白线表示图像的中间,红线表示视图中的固定点,而水平红线后面的蓝线表示距离图像顶部 20% 的位置。 (演示项目在GitHub

    这是显示提供的完整图像和从静止图像过渡的方形框架中的视频的结果。 .

    MainActivity.kt
    prepareMatrix() 是确定如何缩放/裁剪图像的方法。视频还有一些额外的工作要做,因为当视频被分配给TextureView 时,它似乎适合TextureView 作为比例类型“FIT_XY”。由于这种缩放,必须在为视频调用 prepareMatrix() 之前恢复媒体大小

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

    【讨论】:

    猜你喜欢
    • 2019-06-18
    • 2022-06-28
    • 1970-01-01
    • 2018-09-03
    • 1970-01-01
    • 1970-01-01
    • 2022-06-25
    • 2017-07-18
    • 2019-02-25
    相关资源
    最近更新 更多