如何通过中心裁剪和适合 width/height 使视频适合动态壁纸?
How to fit video in Live wallpaper, by center-crop and by fitting to width/height?
背景
我正在制作可以显示视频的动态壁纸。一开始我认为这会很困难,所以有人建议使用 OpenGL 解决方案或其他非常复杂的解决方案(例如 this one)。
无论如何,为此,我找到了很多地方都在谈论它,并且基于这个github library(它有一些错误),我终于让它工作了。
问题
虽然我已成功显示视频,但与屏幕分辨率相比,我找不到控制视频显示方式的方法。
目前它总是被拉伸到屏幕大小,这意味着这个(视频取自 here):
得到显示如下:
原因是宽高比不同:560x320(视频分辨率)与 1080x1920(设备分辨率)。
注意:我很清楚各种 Github 存储库(例如 here)上提供的缩放视频的解决方案,但我问的是动态壁纸。因此,它没有视图,因此它在如何做事方面受到更多限制。更具体地说,解决方案不能有任何类型的布局、TextureView 或 SurfaceView,或任何其他类型的视图。
我试过的
我尝试使用 SurfaceHolder 的各种字段和功能,但到目前为止没有运气。示例:
setVideoScalingMode - 它要么崩溃要么什么都不做。
改变 surfaceFrame - 相同。
这是我制作的当前代码(完整项目可用 here):
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 的内容调整内容的比例,同时保持纵横比:
- center-crop - 适合 100% 的容器(在本例中为屏幕),需要时在侧面(顶部和底部或左侧和右侧)进行裁剪。不拉伸任何东西。这意味着内容看起来不错,但可能不会显示所有内容。
- fit-center - 拉伸以适应 width/height
- center-inside - 设置为原始大小、居中并拉伸以适合 width/height 仅当太大时。
你可以使用Glide for GIF and image loading and its give scaling options as you like. Based on document https://bumptech.github.io/glide/doc/targets.html#sizes-and-dimensions and https://futurestud.io/tutorials/glide-image-resizing-scaling这个。
Glide v4 需要 Android 冰淇淋三明治(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);
}
您可以使用 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) {
}
}
之后使用这个 class 就像下面的代码 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>
中间裁剪代码的输出 看起来像
所以我还不能得到你要求的所有比例类型,但我已经能够使用 exo 播放器相当容易地得到 fit-xy 和 center-crop 的工作。完整的代码可以在 https://github.com/yperess/Whosebug/tree/50091878 看到,我会在获得更多信息时更新它。最后,我还将填充 MainActivity 以允许您选择缩放类型作为设置(我将使用简单的 PreferenceActivity 执行此操作)并读取服务端的共享首选项值。
总体思路是,深入了解 MediaCodec 已经实现了 fit-xy 和 center-crop,如果您有权访问视图层次结构,这实际上是您唯一需要的两种模式。之所以如此,是因为 fit-center、fit-top、fit-bottom 实际上都只是 fit-xy,其中表面具有重力并被缩放以匹配视频大小 * 最小缩放比例。为了使这些工作正常,我认为需要发生的事情是我们需要创建一个 OpenGL 上下文并提供一个 SurfaceTexture。这个 SurfaceTexture 可以用一个可以传递给 exo 播放器的存根 Surface 包裹。加载视频后,我们可以设置它们的大小,因为我们创建了它们。我们还在 SurfaceTexture 上有一个回调,让我们知道框架何时准备就绪。此时我们应该能够修改框架(希望只使用简单的矩阵缩放和变换)。
这里的关键组件是创建 exo 播放器:
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))
}
更新:
成功了,我需要明天在我 post 代码之前清理它,但这是一个预览...
我最终所做的基本上是将 GLSurfaceView 拆开。如果您查看它的源代码,唯一缺少的是它无法在墙纸中使用的事实是它仅在附加到 window 时才启动 GLThread。因此,如果您复制相同的代码但允许手动启动 GLThread,您可以继续。之后,您只需要在缩放到适合的最小比例并移动您绘制的四边形后跟踪屏幕与视频的大小。
代码的已知问题:
1. GLThread 有一个小错误我没能找出来。似乎有一个简单的计时问题,当线程暂停时,我收到对 signallAll()
的调用,实际上并没有在等待任何事情。
2.我没有费心在渲染器中动态修改模式。它不应该太难。在创建引擎时添加首选项侦听器,然后在 scale_type
更改时更新渲染器。
更新:
所有问题都已解决。 signallAll()
正在抛出错误,因为我错过了检查我们是否确实拥有锁的检查。我还添加了一个侦听器来动态更新比例类型,所以现在所有比例类型都使用 GlEngine。
尽情享受吧!
我的解决方案是在动态壁纸
中使用gif(大小和fps与视频相同)而不是视频
看我的回答:,WallpaperService最适合gif
在计算机中使用 ffmpeg 将视频转换为 gif
或在android中,视频可以在android中转换为gif 代码:见
我找到这篇文章:How to set video as live wallpaper and keep video aspect ratio(width and height)
以上文章来源简单,只需点击 "set wallpaper" 按钮,如果您想要全功能应用,请参阅 https://github.com/AlynxZhou/alynx-live-wallpaper
重点是用glsurfaceview代替wallpaperservice默认的surfaceview,制作自定义的glsurfaceview renderer,glsurfaceview可以用opengl来显示,所以问题变成"how to use glsurfaceview play video"或"how to use opengl play video"
如何使用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();
}
}
背景
我正在制作可以显示视频的动态壁纸。一开始我认为这会很困难,所以有人建议使用 OpenGL 解决方案或其他非常复杂的解决方案(例如 this one)。
无论如何,为此,我找到了很多地方都在谈论它,并且基于这个github library(它有一些错误),我终于让它工作了。
问题
虽然我已成功显示视频,但与屏幕分辨率相比,我找不到控制视频显示方式的方法。
目前它总是被拉伸到屏幕大小,这意味着这个(视频取自 here):
得到显示如下:
原因是宽高比不同:560x320(视频分辨率)与 1080x1920(设备分辨率)。
注意:我很清楚各种 Github 存储库(例如 here)上提供的缩放视频的解决方案,但我问的是动态壁纸。因此,它没有视图,因此它在如何做事方面受到更多限制。更具体地说,解决方案不能有任何类型的布局、TextureView 或 SurfaceView,或任何其他类型的视图。
我试过的
我尝试使用 SurfaceHolder 的各种字段和功能,但到目前为止没有运气。示例:
setVideoScalingMode - 它要么崩溃要么什么都不做。
改变 surfaceFrame - 相同。
这是我制作的当前代码(完整项目可用 here):
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 的内容调整内容的比例,同时保持纵横比:
- center-crop - 适合 100% 的容器(在本例中为屏幕),需要时在侧面(顶部和底部或左侧和右侧)进行裁剪。不拉伸任何东西。这意味着内容看起来不错,但可能不会显示所有内容。
- fit-center - 拉伸以适应 width/height
- center-inside - 设置为原始大小、居中并拉伸以适合 width/height 仅当太大时。
你可以使用Glide for GIF and image loading and its give scaling options as you like. Based on document https://bumptech.github.io/glide/doc/targets.html#sizes-and-dimensions and https://futurestud.io/tutorials/glide-image-resizing-scaling这个。
Glide v4 需要 Android 冰淇淋三明治(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);
}
您可以使用 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) {
}
}
之后使用这个 class 就像下面的代码 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>
中间裁剪代码的输出 看起来像
所以我还不能得到你要求的所有比例类型,但我已经能够使用 exo 播放器相当容易地得到 fit-xy 和 center-crop 的工作。完整的代码可以在 https://github.com/yperess/Whosebug/tree/50091878 看到,我会在获得更多信息时更新它。最后,我还将填充 MainActivity 以允许您选择缩放类型作为设置(我将使用简单的 PreferenceActivity 执行此操作)并读取服务端的共享首选项值。
总体思路是,深入了解 MediaCodec 已经实现了 fit-xy 和 center-crop,如果您有权访问视图层次结构,这实际上是您唯一需要的两种模式。之所以如此,是因为 fit-center、fit-top、fit-bottom 实际上都只是 fit-xy,其中表面具有重力并被缩放以匹配视频大小 * 最小缩放比例。为了使这些工作正常,我认为需要发生的事情是我们需要创建一个 OpenGL 上下文并提供一个 SurfaceTexture。这个 SurfaceTexture 可以用一个可以传递给 exo 播放器的存根 Surface 包裹。加载视频后,我们可以设置它们的大小,因为我们创建了它们。我们还在 SurfaceTexture 上有一个回调,让我们知道框架何时准备就绪。此时我们应该能够修改框架(希望只使用简单的矩阵缩放和变换)。
这里的关键组件是创建 exo 播放器:
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))
}
更新:
成功了,我需要明天在我 post 代码之前清理它,但这是一个预览...
我最终所做的基本上是将 GLSurfaceView 拆开。如果您查看它的源代码,唯一缺少的是它无法在墙纸中使用的事实是它仅在附加到 window 时才启动 GLThread。因此,如果您复制相同的代码但允许手动启动 GLThread,您可以继续。之后,您只需要在缩放到适合的最小比例并移动您绘制的四边形后跟踪屏幕与视频的大小。
代码的已知问题:
1. GLThread 有一个小错误我没能找出来。似乎有一个简单的计时问题,当线程暂停时,我收到对 signallAll()
的调用,实际上并没有在等待任何事情。
2.我没有费心在渲染器中动态修改模式。它不应该太难。在创建引擎时添加首选项侦听器,然后在 scale_type
更改时更新渲染器。
更新:
所有问题都已解决。 signallAll()
正在抛出错误,因为我错过了检查我们是否确实拥有锁的检查。我还添加了一个侦听器来动态更新比例类型,所以现在所有比例类型都使用 GlEngine。
尽情享受吧!
我的解决方案是在动态壁纸
中使用gif(大小和fps与视频相同)而不是视频看我的回答:,WallpaperService最适合gif
在计算机中使用 ffmpeg 将视频转换为 gif
或在android中,视频可以在android中转换为gif 代码:见
我找到这篇文章:How to set video as live wallpaper and keep video aspect ratio(width and height)
以上文章来源简单,只需点击 "set wallpaper" 按钮,如果您想要全功能应用,请参阅 https://github.com/AlynxZhou/alynx-live-wallpaper
重点是用glsurfaceview代替wallpaperservice默认的surfaceview,制作自定义的glsurfaceview renderer,glsurfaceview可以用opengl来显示,所以问题变成"how to use glsurfaceview play video"或"how to use opengl play video"
如何使用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();
}
}