如何在动态壁纸中通过居中裁剪和按宽度/高度适配来适应视频?

61

背景

我正在制作一个可以展示视频的动态壁纸。一开始,我认为这将是非常困难的,所以有些人建议使用OpenGL或其他非常复杂的解决方案(比如这个)。

无论如何,关于这个问题,我在各种地方找到了相关的讨论,基于这个Github库(其中有一些错误),我最终使它工作了。

问题

虽然我已经成功地展示了一个视频,但我找不到控制它如何显示相对于屏幕分辨率的方法。

目前它总是被拉伸到屏幕大小,意味着这张图片(从这里获取):

enter image description here

被展示成这样:

enter image description here

原因在于不同的宽高比:560x320(视频分辨率)与1080x1920(设备分辨率)。

注:我很清楚可在各种Github存储库中使用的缩放视频解决方案(例如这里),但我想问的是一个动态壁纸。因此,它没有视图,所以在如何做事情方面更加受限制。更具体地说,解决方案不能有任何类型的布局、TextureView或SurfaceView或其他任何类型的视图。

我尝试过的方法

我尝试了修改SurfaceHolder的各种字段和函数,但迄今为止都没有成功。示例:

这里是我编写的当前代码(完整项目可在此处找到):

class MovieLiveWallpaperService : WallpaperService() {
    override fun onCreateEngine(): WallpaperService.Engine {
        return VideoLiveWallpaperEngine()
    }

    private enum class PlayerState {
        NONE, PREPARING, READY, PLAYING
    }

    inner class VideoLiveWallpaperEngine : WallpaperService.Engine() {
        private var mp: MediaPlayer? = null
        private var playerState: PlayerState = PlayerState.NONE

        override fun onSurfaceCreated(holder: SurfaceHolder) {
            super.onSurfaceCreated(holder)
            Log.d("AppLog", "onSurfaceCreated")
            mp = MediaPlayer()
            val mySurfaceHolder = MySurfaceHolder(holder)
            mp!!.setDisplay(mySurfaceHolder)
            mp!!.isLooping = true
            mp!!.setVolume(0.0f, 0.0f)
            mp!!.setOnPreparedListener { mp ->
                playerState = PlayerState.READY
                setPlay(true)
            }
            try {
                //mp!!.setDataSource(this@MovieLiveWallpaperService, Uri.parse("http://techslides.com/demos/sample-videos/small.mp4"))
                mp!!.setDataSource(this@MovieLiveWallpaperService, Uri.parse("android.resource://" + packageName + "/" + R.raw.small))
            } catch (e: Exception) {
            }
        }

        override fun onDestroy() {
            super.onDestroy()
            Log.d("AppLog", "onDestroy")
            if (mp == null)
                return
            mp!!.stop()
            mp!!.release()
            playerState = PlayerState.NONE
        }

        private fun setPlay(play: Boolean) {
            if (mp == null)
                return
            if (play == mp!!.isPlaying)
                return
            when {
                !play -> {
                    mp!!.pause()
                    playerState = PlayerState.READY
                }
                mp!!.isPlaying -> return
                playerState == PlayerState.READY -> {
                    Log.d("AppLog", "ready, so starting to play")
                    mp!!.start()
                    playerState = PlayerState.PLAYING
                }
                playerState == PlayerState.NONE -> {
                    Log.d("AppLog", "not ready, so preparing")
                    mp!!.prepareAsync()
                    playerState = PlayerState.PREPARING
                }
            }
        }

        override fun onVisibilityChanged(visible: Boolean) {
            super.onVisibilityChanged(visible)
            Log.d("AppLog", "onVisibilityChanged:" + visible + " " + playerState)
            if (mp == null)
                return
            setPlay(visible)
        }

    }

    class MySurfaceHolder(private val surfaceHolder: SurfaceHolder) : SurfaceHolder {
        override fun addCallback(callback: SurfaceHolder.Callback) = surfaceHolder.addCallback(callback)

        override fun getSurface() = surfaceHolder.surface!!

        override fun getSurfaceFrame() = surfaceHolder.surfaceFrame

        override fun isCreating(): Boolean = surfaceHolder.isCreating

        override fun lockCanvas(): Canvas = surfaceHolder.lockCanvas()

        override fun lockCanvas(dirty: Rect): Canvas = surfaceHolder.lockCanvas(dirty)

        override fun removeCallback(callback: SurfaceHolder.Callback) = surfaceHolder.removeCallback(callback)

        override fun setFixedSize(width: Int, height: Int) = surfaceHolder.setFixedSize(width, height)

        override fun setFormat(format: Int) = surfaceHolder.setFormat(format)

        override fun setKeepScreenOn(screenOn: Boolean) {}

        override fun setSizeFromLayout() = surfaceHolder.setSizeFromLayout()

        override fun setType(type: Int) = surfaceHolder.setType(type)

        override fun unlockCanvasAndPost(canvas: Canvas) = surfaceHolder.unlockCanvasAndPost(canvas)
    }
}

问题

我想知道如何根据ImageView来调整内容的比例,同时保持纵横比:

  1. center-crop - 适应容器(在这种情况下是屏幕),需要时裁剪两侧(上和下或左和右)。不会拉伸任何内容。这意味着内容看起来很好,但可能不会全部显示。
  2. fit-center - 拉伸以适应宽度/高度。
  3. center-inside - 设置为原始大小,居中,并且仅在太大时拉伸以适合宽度/高度。

你希望这个程序的最低API版本是多少?是否需要支持到动态壁纸? - TheHebrewHammer
@TheHebrewHammer 目前相对较高(minSdkVersion 27),但将来可能会降低。为什么? - android developer
还没有具体原因,只是想在周一开始之前收集所有相关信息。我已经用OpenGL做了很多AR工作,但从未接触过动态壁纸,应该很有趣。 - TheHebrewHammer
@TheHebrewHammer 好的,谢谢。 - android developer
1
好的,几个小时后,我基本上得到了一个详细的解释,说明实现fit-top、fit-center或fit-bottom有多么困难。我稍后还会尝试一下,但我认为现在你只能使用fit-xy和center-crop。我明天会发布我目前所拥有的内容,并在找到其他模式的合理方法时进行更新。 - TheHebrewHammer
显示剩余27条评论
5个回答

7
你可以使用TextureView实现这一点。(surfaceView也行不通)。我找到了一些代码,可以帮助你实现这一点。
在这个演示中,你可以将视频裁剪成三种类型:居中、顶部和底部。TextureVideoView.java
public class TextureVideoView extends TextureView implements TextureView.SurfaceTextureListener {

    // Indicate if logging is on
    public static final boolean LOG_ON = true;

    // Log tag
    private static final String TAG = TextureVideoView.class.getName();

    private MediaPlayer mMediaPlayer;

    private float mVideoHeight;
    private float mVideoWidth;

    private boolean mIsDataSourceSet;
    private boolean mIsViewAvailable;
    private boolean mIsVideoPrepared;
    private boolean mIsPlayCalled;

    private ScaleType mScaleType;
    private State mState;

    public enum ScaleType {
        CENTER_CROP, TOP, BOTTOM
    }

    public enum State {
        UNINITIALIZED, PLAY, STOP, PAUSE, END
    }

    public TextureVideoView(Context context) {
        super(context);
        initView();
    }

    public TextureVideoView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public TextureVideoView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initView();
    }

    private void initView() {
        initPlayer();
        setScaleType(ScaleType.CENTER_CROP);
        setSurfaceTextureListener(this);
    }

    public void setScaleType(ScaleType scaleType) {
        mScaleType = scaleType;
    }

    private void updateTextureViewSize() {
        float viewWidth = getWidth();
        float viewHeight = getHeight();

        float scaleX = 1.0f;
        float scaleY = 1.0f;

        if (mVideoWidth > viewWidth && mVideoHeight > viewHeight) {
            scaleX = mVideoWidth / viewWidth;
            scaleY = mVideoHeight / viewHeight;
        } else if (mVideoWidth < viewWidth && mVideoHeight < viewHeight) {
            scaleY = viewWidth / mVideoWidth;
            scaleX = viewHeight / mVideoHeight;
        } else if (viewWidth > mVideoWidth) {
            scaleY = (viewWidth / mVideoWidth) / (viewHeight / mVideoHeight);
        } else if (viewHeight > mVideoHeight) {
            scaleX = (viewHeight / mVideoHeight) / (viewWidth / mVideoWidth);
        }

        // Calculate pivot points, in our case crop from center
        int pivotPointX;
        int pivotPointY;

        switch (mScaleType) {
            case TOP:
                pivotPointX = 0;
                pivotPointY = 0;
                break;
            case BOTTOM:
                pivotPointX = (int) (viewWidth);
                pivotPointY = (int) (viewHeight);
                break;
            case CENTER_CROP:
                pivotPointX = (int) (viewWidth / 2);
                pivotPointY = (int) (viewHeight / 2);
                break;
            default:
                pivotPointX = (int) (viewWidth / 2);
                pivotPointY = (int) (viewHeight / 2);
                break;
        }

        Matrix matrix = new Matrix();
        matrix.setScale(scaleX, scaleY, pivotPointX, pivotPointY);

        setTransform(matrix);
    }

    private void initPlayer() {
        if (mMediaPlayer == null) {
            mMediaPlayer = new MediaPlayer();
        } else {
            mMediaPlayer.reset();
        }
        mIsVideoPrepared = false;
        mIsPlayCalled = false;
        mState = State.UNINITIALIZED;
    }

    /**
     * @see MediaPlayer#setDataSource(String)
     */
    public void setDataSource(String path) {
        initPlayer();

        try {
            mMediaPlayer.setDataSource(path);
            mIsDataSourceSet = true;
            prepare();
        } catch (IOException e) {
            Log.d(TAG, e.getMessage());
        }
    }

    /**
     * @see MediaPlayer#setDataSource(Context, Uri)
     */
    public void setDataSource(Context context, Uri uri) {
        initPlayer();

        try {
            mMediaPlayer.setDataSource(context, uri);
            mIsDataSourceSet = true;
            prepare();
        } catch (IOException e) {
            Log.d(TAG, e.getMessage());
        }
    }

    /**
     * @see MediaPlayer#setDataSource(java.io.FileDescriptor)
     */
    public void setDataSource(AssetFileDescriptor afd) {
        initPlayer();

        try {
            long startOffset = afd.getStartOffset();
            long length = afd.getLength();
            mMediaPlayer.setDataSource(afd.getFileDescriptor(), startOffset, length);
            mIsDataSourceSet = true;
            prepare();
        } catch (IOException e) {
            Log.d(TAG, e.getMessage());
        }
    }

    private void prepare() {
        try {
            mMediaPlayer.setOnVideoSizeChangedListener(
                    new MediaPlayer.OnVideoSizeChangedListener() {
                        @Override
                        public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
                            mVideoWidth = width;
                            mVideoHeight = height;
                            updateTextureViewSize();
                        }
                    }
            );
            mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mp) {
                    mState = State.END;
                    log("Video has ended.");

                    if (mListener != null) {
                        mListener.onVideoEnd();
                    }
                }
            });

            // don't forget to call MediaPlayer.prepareAsync() method when you use constructor for
            // creating MediaPlayer
            mMediaPlayer.prepareAsync();

            // Play video when the media source is ready for playback.
            mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                @Override
                public void onPrepared(MediaPlayer mediaPlayer) {
                    mIsVideoPrepared = true;
                    if (mIsPlayCalled && mIsViewAvailable) {
                        log("Player is prepared and play() was called.");
                        play();
                    }

                    if (mListener != null) {
                        mListener.onVideoPrepared();
                    }
                }
            });

        } catch (IllegalArgumentException e) {
            Log.d(TAG, e.getMessage());
        } catch (SecurityException e) {
            Log.d(TAG, e.getMessage());
        } catch (IllegalStateException e) {
            Log.d(TAG, e.toString());
        }
    }

    /**
     * Play or resume video. Video will be played as soon as view is available and media player is
     * prepared.
     *
     * If video is stopped or ended and play() method was called, video will start over.
     */
    public void play() {
        if (!mIsDataSourceSet) {
            log("play() was called but data source was not set.");
            return;
        }

        mIsPlayCalled = true;

        if (!mIsVideoPrepared) {
            log("play() was called but video is not prepared yet, waiting.");
            return;
        }

        if (!mIsViewAvailable) {
            log("play() was called but view is not available yet, waiting.");
            return;
        }

        if (mState == State.PLAY) {
            log("play() was called but video is already playing.");
            return;
        }

        if (mState == State.PAUSE) {
            log("play() was called but video is paused, resuming.");
            mState = State.PLAY;
            mMediaPlayer.start();
            return;
        }

        if (mState == State.END || mState == State.STOP) {
            log("play() was called but video already ended, starting over.");
            mState = State.PLAY;
            mMediaPlayer.seekTo(0);
            mMediaPlayer.start();
            return;
        }

        mState = State.PLAY;
        mMediaPlayer.start();
    }

    /**
     * Pause video. If video is already paused, stopped or ended nothing will happen.
     */
    public void pause() {
        if (mState == State.PAUSE) {
            log("pause() was called but video already paused.");
            return;
        }

        if (mState == State.STOP) {
            log("pause() was called but video already stopped.");
            return;
        }

        if (mState == State.END) {
            log("pause() was called but video already ended.");
            return;
        }

        mState = State.PAUSE;
        if (mMediaPlayer.isPlaying()) {
            mMediaPlayer.pause();
        }
    }

    /**
     * Stop video (pause and seek to beginning). If video is already stopped or ended nothing will
     * happen.
     */
    public void stop() {
        if (mState == State.STOP) {
            log("stop() was called but video already stopped.");
            return;
        }

        if (mState == State.END) {
            log("stop() was called but video already ended.");
            return;
        }

        mState = State.STOP;
        if (mMediaPlayer.isPlaying()) {
            mMediaPlayer.pause();
            mMediaPlayer.seekTo(0);
        }
    }

    /**
     * @see MediaPlayer#setLooping(boolean)
     */
    public void setLooping(boolean looping) {
        mMediaPlayer.setLooping(looping);
    }

    /**
     * @see MediaPlayer#seekTo(int)
     */
    public void seekTo(int milliseconds) {
        mMediaPlayer.seekTo(milliseconds);
    }

    /**
     * @see MediaPlayer#getDuration()
     */
    public int getDuration() {
        return mMediaPlayer.getDuration();
    }

    static void log(String message) {
        if (LOG_ON) {
            Log.d(TAG, message);
        }
    }

    private MediaPlayerListener mListener;

    /**
     * Listener trigger 'onVideoPrepared' and `onVideoEnd` events
     */
    public void setListener(MediaPlayerListener listener) {
        mListener = listener;
    }

    public interface MediaPlayerListener {

        public void onVideoPrepared();

        public void onVideoEnd();
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
        Surface surface = new Surface(surfaceTexture);
        mMediaPlayer.setSurface(surface);
        mIsViewAvailable = true;
        if (mIsDataSourceSet && mIsPlayCalled && mIsVideoPrepared) {
            log("View is available and play() was called.");
            play();
        }
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        return false;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {

    }
}

接下来,在MainActivity.java中像下面的代码一样使用这个类:

public class MainActivity extends AppCompatActivity implements View.OnClickListener,
        ActionBar.OnNavigationListener {

    // Video file url
    private static final String FILE_URL = "http://techslides.com/demos/sample-videos/small.mp4";
    private TextureVideoView mTextureVideoView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
        initActionBar();

        if (!isWIFIOn(getBaseContext())) {
            Toast.makeText(getBaseContext(), "You need internet connection to stream video",
                    Toast.LENGTH_LONG).show();
        }
    }

    private void initActionBar() {
        ActionBar actionBar = getSupportActionBar();
        actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
        actionBar.setDisplayShowTitleEnabled(false);

        SpinnerAdapter mSpinnerAdapter = ArrayAdapter.createFromResource(this, R.array.action_list,
                android.R.layout.simple_spinner_dropdown_item);
        actionBar.setListNavigationCallbacks(mSpinnerAdapter, this);
    }

    private void initView() {
        mTextureVideoView = (TextureVideoView) findViewById(R.id.cropTextureView);

        findViewById(R.id.btnPlay).setOnClickListener(this);
        findViewById(R.id.btnPause).setOnClickListener(this);
        findViewById(R.id.btnStop).setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btnPlay:
                mTextureVideoView.play();
                break;
            case R.id.btnPause:
                mTextureVideoView.pause();
                break;
            case R.id.btnStop:
                mTextureVideoView.stop();
                break;
        }
    }

    final int indexCropCenter = 0;
    final int indexCropTop = 1;
    final int indexCropBottom = 2;

    @Override
    public boolean onNavigationItemSelected(int itemPosition, long itemId) {
        switch (itemPosition) {
            case indexCropCenter:
                mTextureVideoView.stop();
                mTextureVideoView.setScaleType(TextureVideoView.ScaleType.CENTER_CROP);
                mTextureVideoView.setDataSource(FILE_URL);
                mTextureVideoView.play();
                break;
            case indexCropTop:
                mTextureVideoView.stop();
                mTextureVideoView.setScaleType(TextureVideoView.ScaleType.TOP);
                mTextureVideoView.setDataSource(FILE_URL);
                mTextureVideoView.play();
                break;
            case indexCropBottom:
                mTextureVideoView.stop();
                mTextureVideoView.setScaleType(TextureVideoView.ScaleType.BOTTOM);
                mTextureVideoView.setDataSource(FILE_URL);
                mTextureVideoView.play();
                break;
        }
        return true;
    }

    public static boolean isWIFIOn(Context context) {
        ConnectivityManager connMgr =
                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI);

        return (networkInfo != null && networkInfo.isConnected());
    }
}

以下是布局 activity_main.xml 文件的代码:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <com.example.videocropdemo.crop.TextureVideoView
        android:id="@+id/cropTextureView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_centerInParent="true" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_margin="16dp"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btnPlay"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Play" />

        <Button
            android:id="@+id/btnPause"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Pause" />

        <Button
            android:id="@+id/btnStop"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Stop" />
    </LinearLayout>
</RelativeLayout>

代码运行后的输出如下,进行中心裁剪:enter image description here

1
这很好,但我在标题和描述中明确写了,我对如何为实时壁纸做这件事感兴趣。 它里面没有视图。 它甚至没有布局文件。 - android developer

4
所以我还不能获取您要求的所有比例类型,但是使用exo player可以相当容易地实现fit-xy和center-crop。完整代码可以在https://github.com/yperess/StackOverflow/tree/50091878中查看,并且我会随着进展更新它。最终,我还将填写MainActivity,允许您选择缩放类型作为设置(我将使用简单的PreferenceActivity完成此操作),并在服务端读取共享首选项值。
总体思路是,深层MediaCodec已经实现了fit-xy和center-crop这两种模式,如果您可以访问视图层次结构,则真正需要的只有这两种模式。这是因为fit-center、fit-top和fit-bottom都只是fit-xy,其中表面具有重力并缩放以匹配视频大小*最小缩放。为了使它们起作用,我认为需要做的是创建OpenGL上下文并提供SurfaceTexture。这个SurfaceTexture可以用一个存根Surface包装,然后传递给exo player。一旦加载视频,我们就可以设置它们的大小,因为我们创建了它们。我们还有一个回调函数在SurfaceTexture上,让我们知道何时准备好一帧。此时,我们应该能够修改帧(希望只使用简单的矩阵缩放和变换)。
这里的关键组件是创建exo player:
    private fun initExoMediaPlayer(): SimpleExoPlayer {
        val videoTrackSelectionFactory = AdaptiveTrackSelection.Factory(bandwidthMeter)
        val trackSelector = DefaultTrackSelector(videoTrackSelectionFactory)
        val player = ExoPlayerFactory.newSimpleInstance(this@MovieLiveWallpaperService,
                trackSelector)
        player.playWhenReady = true
        player.repeatMode = Player.REPEAT_MODE_ONE
        player.volume = 0f
        if (mode == Mode.CENTER_CROP) {
            player.videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
        } else {
            player.videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT
        }
        if (mode == Mode.FIT_CENTER) {
            player.addVideoListener(this)
        }
        return player
    }

然后加载视频:

    override fun onSurfaceCreated(holder: SurfaceHolder) {
        super.onSurfaceCreated(holder)
        if (mode == Mode.FIT_CENTER) {
            // We need to somehow wrap the surface or set some scale factor on exo player here.
            // Most likely this will require creating a SurfaceTexture and attaching it to an
            // OpenGL context. Then for each frame, writing it to the original surface but with
            // an offset
            exoMediaPlayer.setVideoSurface(holder.surface)
        } else {
            exoMediaPlayer.setVideoSurfaceHolder(holder)
        }

        val videoUri = RawResourceDataSource.buildRawResourceUri(R.raw.small)
        val dataSourceFactory = DataSource.Factory { RawResourceDataSource(context) }
        val mediaSourceFactory = ExtractorMediaSource.Factory(dataSourceFactory)
        exoMediaPlayer.prepare(mediaSourceFactory.createMediaSource(videoUri))
    }

更新:

搞定了,我需要明天整理一下代码再发布,但这里是一个预览... fit_center

我最终做的事情基本上就是拆开GLSurfaceView。如果你查看它的源代码,唯一缺失的东西是它只有在附加到窗口时才启动GLThread,这使得它不可能用于壁纸。因此,如果你复制相同的代码但允许手动启动GLThread,那么你可以继续进行。之后,你只需要跟踪屏幕大小与视频大小的比例,缩放到最小比例并移动绘制图形的四边形即可。

代码存在的已知问题: 1. GLThread 存在一个小bug,我还没有找到解决方法。似乎有一个简单的时间问题,当线程暂停时,我会收到一个不等待任何东西的 signallAll() 调用。 2. 我没有费心动态修改渲染器中的模式。这应该不难。创建引擎时添加首选项监听器,然后在 scale_type 更改时更新渲染器。

更新: 所有问题都已解决。 signallAll() 抛出异常是因为我忘记检查我们是否真的拥有锁。我还添加了一个监听器,以动态更新比例类型,现在所有比例类型都使用 GlEngine。

享受吧!


是的,这也是我在 ExoPlayer 的 Github 上与他们交谈时得到的结论:https://github.com/google/ExoPlayer/issues/4457。但是有些应用程序如何成功地以其他方式显示视频缩放呢? - android developer
目前已经实现了中心裁剪(保持宽高比,两边都大于等于屏幕)和适应XY(两边等于屏幕,失去宽高比),我还在开发适应中心的功能,它将保持宽高比但填充高度/宽度并将视频居中,这个功能应该足够相似,我可能会同时实现适应底部和适应顶部。我会尽力完成,但明天早上我要带女儿去徒步旅行5天,所以我们会看到我是否能按时完成。无论如何,我一定会解决这个问题 :) - TheHebrewHammer
但是我得到的是fixXY,这几乎从来没有被真正使用过...你会如何做fit-center? - android developer
我现在正在做的是创建一个SurfaceTexture,并让exoplayer向其写入内容。每一帧,我应该能够在OpenGL中复制帧缓冲区。然后,在应用缩放和变换后,我“应该”能将该缓冲区写入壁纸的表面。理论上所有的部分都应该可以工作... 然而,理论总是美好的。等我尝试完之后再发布结果。 - TheHebrewHammer
这似乎是一个不错的计划,但在效率方面会有所不同吗?我不希望它消耗太多电池... - android developer
显示剩余18条评论

1
I find this article: 如何将视频设置为动态壁纸并保持宽高比(宽度和高度) 以上文章源码简单,只需点击“设置壁纸”按钮即可,如果您想要完整功能的应用程序,请参见https://github.com/AlynxZhou/alynx-live-wallpaper 关键是使用glsurfaceview而不是wallpaperservice默认的surfaceview,制作自定义glsurfaceview渲染器,glsurfaceview可以使用OpenGL显示,因此问题变成了“如何使用glsurfaceview播放视频”或“如何使用OpenGL播放视频”
如何使用glsurfaceview替换wallpaperservice默认的surfaceview:
public class GLWallpaperService extends WallpaperService {
...

    class GLWallpaperEngine extends Engine {
...

        private class GLWallpaperSurfaceView extends GLSurfaceView {
            @SuppressWarnings("unused")
            private static final String TAG = "GLWallpaperSurface";

            public GLWallpaperSurfaceView(Context context) {
                super(context);
            }

            /**
             * This is a hack. Because Android Live Wallpaper only has a Surface.
             * So we create a GLSurfaceView, and when drawing to its Surface,
             * we replace it with WallpaperEngine's Surface.
             */
            @Override
            public SurfaceHolder getHolder() {
                return getSurfaceHolder();
            }

            void onDestroy() {
                super.onDetachedFromWindow();
            }
        }

您能否分享整个代码?由于某些原因,我无法访问它。 - android developer
1
@androiddeveloper 请查看 https://github.com/AlynxZhou/alynx-live-wallpaper - chikadance
1
这很不错,但似乎比我问的更多。我只是问了一个具体的问题。你能指出哪些部分我应该看吗?或者写一个最小的应用程序来演示它并在这里发布?如果听起来很无礼,请原谅。我已经给你+1了。希望没关系。 - android developer
1
@androiddeveloper,请查看此文件:https://github.com/AlynxZhou/alynx-live-wallpaper/blob/master/app/src/main/java/xyz/alynx/livewallpaper/GLWallpaperService.java,它是壁纸服务,使用exoplayer,将exoplayer的视频源更改为您的视频,并将此壁纸服务设置为动态壁纸。很抱歉我不知道如何使用OpenGL,但我确定这个存储库可以工作。 - chikadance
1
是的,我知道它可以工作。只是希望有一个更简洁的解决方案。我会有一天去尝试它。抱歉并感谢。 - android developer
很遗憾,这个示例没有提供各种比例类型选项。事实上,它只有中心裁剪,可以将其设置为滑动选项... 有什么办法可以添加其他比例类型呢? - android developer

0

GIF 在性能和文件大小方面都被认为不够高效。动态 WEBP 也是如此。 - android developer

-9

你可以使用Glide来加载GIF和图片,并根据需要进行缩放。根据文档https://bumptech.github.io/glide/doc/targets.html#sizes-and-dimensionshttps://futurestud.io/tutorials/glide-image-resizing-scaling

Glide v4需要Android Ice Cream Sandwich(API级别14)或更高版本。

类似于:

public static void loadCircularImageGlide(String imagePath, ImageView view) {
    Glide.with(view.getContext())
            .load(imagePath)
            .asGif()
            .override(600, 200) // resizes the image to these dimensions (in pixel). resize does not respect aspect ratio
            .error(R.drawable.create_timeline_placeholder)
            .fitCenter() // scaling options
            .transform(new CircularTransformation(view.getContext())) // Even you can Give image tranformation too
            .into(view);
}

3
我知道这件事了,但这次没有视图。这是一张动态壁纸,请演示如何设置动态壁纸,提供完整的代码,就像我展示的那样。 - android developer
@androiddeveloper 如果您需要完整的代码,则需要与我分享您的代码。 - Mayur Patel
1
完整的代码从一开始就一直可用。请检查链接。自昨天起,它甚至在Github上可用。 - android developer
9
此外,问题是关于视频,而不是 GIF。 - android developer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接