版本:1.0
日期:2014.11.25 2014.11.26
版权:©kince
特别推荐:泡在网上的日子
一、概述
一般Launcher都带有壁纸设置的功能,Android提供了设置壁纸的API,在包android.app下面的类WallpaperInfo和WallpaperManager。动态壁纸所在的包是android.service.wallpaper,要区别开。但是要注意,WallpaperInfo是描述动态壁纸的类,从WallpaperManager类的getWallpaperInfo()方法获取的返回值就是WallpaperInfo类对象。关于设置静态壁纸的类,主要就是WallpaperManager这个类。它有setBitmap(Bitmap bitmap)、setResource(int resid)、setStream(InputStream data)等方法设置静态壁纸。本文主要讨论静态壁纸的设置,包括固定不动和随着桌面滑动的壁纸。
二、Launcher壁纸
设置
下面以KitKat为例,说一下Launcher设置壁纸的流程。在Launcher类的菜单中有设置壁纸的功能,代码如下:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_WALLPAPER_SETTINGS :
startWallpaper();
return true ;
}
return super .onOptionsItemSelected(item);
}
进入startWallpaper()方法,
private void startWallpaper () {
showWorkspace( true);
final Intent pickWallpaper = new Intent(Intent.ACTION_SET_WALLPAPER );
Intent chooser = Intent.createChooser(pickWallpaper,
getText(R.string. chooser_wallpaper));
// NOTE: Adds a configure option to the chooser if the wallpaper supports it
// Removed in Eclair MR1
// WallpaperManager wm = (WallpaperManager)
// getSystemService(Context.WALLPAPER_SERVICE);
// WallpaperInfo wi = wm.getWallpaperInfo();
// if ( wi != null && wi.getSettingsActivity() != null) {
// LabeledIntent li = new LabeledIntent(getPackageName(),
// R.string.configure_wallpaper, 0);
// li.setClassName(wi.getPackageName(), wi.getSettingsActivity());
// chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { li });
// }
startActivityForResult(chooser, REQUEST_PICK_WALLPAPER );
}
上面这个方法会传递一个选择壁纸的Intent,然后使用Intent.createChooser打开一个界面选择具备设置壁纸的应用。
图-1
每一个应用都有自己设置壁纸的功能,而且也不尽相同。360是这样:
图-2
原生图库是这样:
图-3
那下面就以原生图库为例来梳理一下这个流程,还是以KitKat版本为例,找到原生图库的代码Gallery2,地址是
https://android.googlesource.com/platform/packages/apps/Gallery2/
。不过这个地址有些奇怪,从中下载的代码不完全,少了几个类。还是从
http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android-apps/4.4_r1/com/android/gallery3d
这个地址下载其他缺少的类。Gallery2的源码也很多,不得不说谷歌原生app的代码健壮性都还不错,不是一般的强。项目工程大体如下:
图-4
当然大多数还是负责处理显示图片、编辑图片的功能,对于设置壁纸的功能代码不是很多。那么当从选择的界面中选择图库这个应用后,流程是怎么样的呢?之前是发送了一个
Intent.ACTION_SET_WALLPAPER的Intent,那么在图库的应用中必然有个Activity接受这个Intent,而且这个Activity的Intent-filter必然有ACTION_SET_WALLPAPER。在图库的AndroidManifest.xml文件中,搜索SET_WALLPAPER,发现Wallpaper这个类唯一具备这个条件。那么很显然,当我们从选择界面点击图库后,进入的就是这个Activity。
<activity android:name="com.android.gallery3d.app.Wallpaper"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@style/android:Theme.Translucent.NoTitleBar">
<intent-filter android:label="@string/camera_setas_wallpaper">
<action android:name="android.intent.action.ATTACH_DATA" />
<data android:mimeType="image/*" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter android:label="@string/app_name">
<action android:name="android.intent.action.SET_WALLPAPER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="android.wallpaper.preview"
android:resource="@xml/wallpaper_picker_preview" />
</activity>
来到Wallpaper类下,代码如下:
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.app;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.WallpaperManager;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.graphics.Point;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.Display;
import com.android.gallery3d.common.ApiHelper;
import com.android.gallery3d.filtershow.crop.CropActivity;
import com.android.gallery3d.filtershow.crop.CropExtras;
import java.lang.IllegalArgumentException;
/**
* Wallpaper picker for the gallery application. This just redirects to the
* standard pick action.
*/
public class Wallpaper extends Activity {
@SuppressWarnings("unused")
private static final String TAG = "Wallpaper";
private static final String IMAGE_TYPE = "image/*";
private static final String KEY_STATE = "activity-state";
private static final String KEY_PICKED_ITEM = "picked-item";
private static final int STATE_INIT = 0;
private static final int STATE_PHOTO_PICKED = 1;
private int mState = STATE_INIT;
private Uri mPickedItem;
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
if (bundle != null) {
mState = bundle.getInt(KEY_STATE);
mPickedItem = (Uri) bundle.getParcelable(KEY_PICKED_ITEM);
}
}
@Override
protected void onSaveInstanceState(Bundle saveState) {
saveState.putInt(KEY_STATE, mState);
if (mPickedItem != null) {
saveState.putParcelable(KEY_PICKED_ITEM, mPickedItem);
}
}
@SuppressWarnings("deprecation")
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
private Point getDefaultDisplaySize(Point size) {
Display d = getWindowManager().getDefaultDisplay();
if (Build.VERSION.SDK_INT >= ApiHelper.VERSION_CODES.HONEYCOMB_MR2) {
d.getSize(size);
} else {
size.set(d.getWidth(), d.getHeight());
}
return size;
}
@SuppressWarnings("fallthrough")
@Override
protected void onResume() {
super.onResume();
Intent intent = getIntent();
switch (mState) {
case STATE_INIT: {
mPickedItem = intent.getData();
if (mPickedItem == null) {
Intent request = new Intent(Intent.ACTION_GET_CONTENT)
.setClass(this, DialogPicker.class)
.setType(IMAGE_TYPE);
startActivityForResult(request, STATE_PHOTO_PICKED);
return;
}
mState = STATE_PHOTO_PICKED;
// fall-through
}
case STATE_PHOTO_PICKED: {
Intent cropAndSetWallpaperIntent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WallpaperManager wpm = WallpaperManager.getInstance(getApplicationContext());
try {
cropAndSetWallpaperIntent = wpm.getCropAndSetWallpaperIntent(mPickedItem);
startActivity(cropAndSetWallpaperIntent);
finish();
return;
} catch (ActivityNotFoundException anfe) {
// ignored; fallthru to existing crop activity
} catch (IllegalArgumentException iae) {
// ignored; fallthru to existing crop activity
}
}
int width = getWallpaperDesiredMinimumWidth();
int height = getWallpaperDesiredMinimumHeight();
Point size = getDefaultDisplaySize(new Point());
float spotlightX = (float) size.x / width;
float spotlightY = (float) size.y / height;
cropAndSetWallpaperIntent = new Intent(CropActivity.CROP_ACTION)
.setClass(this, CropActivity.class)
.setDataAndType(mPickedItem, IMAGE_TYPE)
.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
.putExtra(CropExtras.KEY_OUTPUT_X, width)
.putExtra(CropExtras.KEY_OUTPUT_Y, height)
.putExtra(CropExtras.KEY_ASPECT_X, width)
.putExtra(CropExtras.KEY_ASPECT_Y, height)
.putExtra(CropExtras.KEY_SPOTLIGHT_X, spotlightX)
.putExtra(CropExtras.KEY_SPOTLIGHT_Y, spotlightY)
.putExtra(CropExtras.KEY_SCALE, true)
.putExtra(CropExtras.KEY_SCALE_UP_IF_NEEDED, true)
.putExtra(CropExtras.KEY_SET_AS_WALLPAPER, true);
startActivity(cropAndSetWallpaperIntent);
finish();
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != RESULT_OK) {
setResult(resultCode);
finish();
return;
}
mState = requestCode;
if (mState == STATE_PHOTO_PICKED) {
mPickedItem = data.getData();
}
// onResume() would be called next
}
}
这个类的逻辑比较简单,先是第一次进入Activity然后进入onResume()方法,然后进入 STATE_INIT。在这里选择一张照片,然后返回。接着调用onActivityResult方法,用于获取mPickedItem值,之后再次调用onResume()方法,然后进入STATE_PHOTO_PICKED。先是一个判断,如果是KitKat以上版本,那么会调用新的方法getCropAndSetWallpaperIntent去完成壁纸的设置,界面如下:
图-5
如果是KitKat以下版本,那么会调用以下方法:
int width = getWallpaperDesiredMinimumWidth();
int height = getWallpaperDesiredMinimumHeight();
Point size = getDefaultDisplaySize( new Point());
float spotlightX = (float) size.x / width;
float spotlightY = (float) size.y / height;
cropAndSetWallpaperIntent = new Intent(CropActivity.CROP_ACTION )
.setClass( this, CropActivity.class)
.setDataAndType( mPickedItem, IMAGE_TYPE)
.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT )
.putExtra(CropExtras. KEY_OUTPUT_X, width)
.putExtra(CropExtras. KEY_OUTPUT_Y, height)
.putExtra(CropExtras. KEY_ASPECT_X, width)
.putExtra(CropExtras. KEY_ASPECT_Y, height)
.putExtra(CropExtras. KEY_SPOTLIGHT_X, spotlightX)
.putExtra(CropExtras. KEY_SPOTLIGHT_Y, spotlightY)
.putExtra(CropExtras. KEY_SCALE, true )
.putExtra(CropExtras.KEY_SCALE_UP_IF_NEEDED , true)
.putExtra(CropExtras.KEY_SET_AS_WALLPAPER , true);
startActivity(cropAndSetWallpaperIntent);
finish();
先是调用系统方法获取期望的壁纸的最小宽度和最小高度。然后是
CropExtras
这个类,这个实体类描述了剪裁壁纸的信息,包括长宽、缩放、格式等等。代码如下:
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.filtershow.crop;
import android.net.Uri;
public class CropExtras {
public static final String KEY_CROPPED_RECT = "cropped-rect";
public static final String KEY_OUTPUT_X = "outputX";
public static final String KEY_OUTPUT_Y = "outputY";
public static final String KEY_SCALE = "scale";
public static final String KEY_SCALE_UP_IF_NEEDED = "scaleUpIfNeeded";
public static final String KEY_ASPECT_X = "aspectX";
public static final String KEY_ASPECT_Y = "aspectY";
public static final String KEY_SET_AS_WALLPAPER = "set-as-wallpaper";
public static final String KEY_RETURN_DATA = "return-data";
public static final String KEY_DATA = "data";
public static final String KEY_SPOTLIGHT_X = "spotlightX";
public static final String KEY_SPOTLIGHT_Y = "spotlightY";
public static final String KEY_SHOW_WHEN_LOCKED = "showWhenLocked";
public static final String KEY_OUTPUT_FORMAT = "outputFormat";
private int mOutputX = 0;
private int mOutputY = 0;
private boolean mScaleUp = true;
private int mAspectX = 0;
private int mAspectY = 0;
private boolean mSetAsWallpaper = false;
private boolean mReturnData = false;
private Uri mExtraOutput = null;
private String mOutputFormat = null;
private boolean mShowWhenLocked = false;
private float mSpotlightX = 0;
private float mSpotlightY = 0;
public CropExtras(int outputX, int outputY, boolean scaleUp, int aspectX, int aspectY,
boolean setAsWallpaper, boolean returnData, Uri extraOutput, String outputFormat,
boolean showWhenLocked, float spotlightX, float spotlightY) {
mOutputX = outputX;
mOutputY = outputY;
mScaleUp = scaleUp;
mAspectX = aspectX;
mAspectY = aspectY;
mSetAsWallpaper = setAsWallpaper;
mReturnData = returnData;
mExtraOutput = extraOutput;
mOutputFormat = outputFormat;
mShowWhenLocked = showWhenLocked;
mSpotlightX = spotlightX;
mSpotlightY = spotlightY;
}
public CropExtras(CropExtras c) {
this(c.mOutputX, c.mOutputY, c.mScaleUp, c.mAspectX, c.mAspectY, c.mSetAsWallpaper,
c.mReturnData, c.mExtraOutput, c.mOutputFormat, c.mShowWhenLocked,
c.mSpotlightX, c.mSpotlightY);
}
public int getOutputX() {
return mOutputX;
}
public int getOutputY() {
return mOutputY;
}
public boolean getScaleUp() {
return mScaleUp;
}
public int getAspectX() {
return mAspectX;
}
public int getAspectY() {
return mAspectY;
}
public boolean getSetAsWallpaper() {
return mSetAsWallpaper;
}
public boolean getReturnData() {
return mReturnData;
}
public Uri getExtraOutput() {
return mExtraOutput;
}
public String getOutputFormat() {
return mOutputFormat;
}
public boolean getShowWhenLocked() {
return mShowWhenLocked;
}
public float getSpotlightX() {
return mSpotlightX;
}
public float getSpotlightY() {
return mSpotlightY;
}
}
不过对于类成员属性我们并不清楚各自代表什么意思,不过从传递进去的参数来看,是可以发现一些端倪的。比如
KEY_OUTPUT_X
现在可以简单的理解为
width,同理高度也一样。
进入
CropActivity的时候会把这些信息传递过去。
CropActivity就是上面那个可以缩放设置壁纸的界面,完成这个功能的那个其实是一个自定义View:
CropView
。接着来到
CropActivity,这就是
图-3所显示的Activity。这个类所在包以及其他类如下:
图-6
可以看到共有七个类一起完成壁纸移动、缩放设置的功能,当然最核心的是
CropActivity,它
负责
统筹调度其他类。代码如下:
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.filtershow.crop;
import android.app.ActionBar;
import android.app.Activity;
import android.app.WallpaperManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.Toast;
import com.android.gallery3d.R;
import com.android.gallery3d.common.Utils;
import com.android.gallery3d.filtershow.cache.ImageLoader;
import com.android.gallery3d.filtershow.tools.SaveImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Activity for cropping an image.
*/
public class CropActivity extends Activity {
private static final String LOGTAG = "CropActivity";
public static final String CROP_ACTION = "com.android.camera.action.CROP";
// 剪裁信息
private CropExtras mCropExtras = null;
// 加载Bitmap
private LoadBitmapTask mLoadBitmapTask = null;
private int mOutputX = 0;
private int mOutputY = 0;
private Bitmap mOriginalBitmap = null;
private RectF mOriginalBounds = null;
private int mOriginalRotation = 0;
private Uri mSourceUri = null;
// 自定义的View 剪裁壁纸
private CropView mCropView = null;
// 保存的button
private View mSaveButton = null;
// 保护IO操作
private boolean finalIOGuard = false;
private static final int SELECT_PICTURE = 1; // request code for picker
private static final int DEFAULT_COMPRESS_QUALITY = 90;
/**
* The maximum bitmap size we allow to be returned through the intent.
* Intents have a maximum of 1MB in total size. However, the Bitmap seems to
* have some overhead to hit so that we go way below the limit here to make
* sure the intent stays below 1MB.We should consider just returning a byte
* array instead of a Bitmap instance to avoid overhead.
*/
public static final int MAX_BMAP_IN_INTENT = 750000;
// Flags
private static final int DO_SET_WALLPAPER = 1;
private static final int DO_RETURN_DATA = 1 << 1;
private static final int DO_EXTRA_OUTPUT = 1 << 2;
private static final int FLAG_CHECK = DO_SET_WALLPAPER | DO_RETURN_DATA | DO_EXTRA_OUTPUT;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//
Intent intent = getIntent();
setResult(RESULT_CANCELED, new Intent());
mCropExtras = getExtrasFromIntent(intent);
//
if (mCropExtras != null && mCropExtras.getShowWhenLocked()) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
}
setContentView(R.layout.crop_activity);
mCropView = (CropView) findViewById(R.id.cropView);
// 自定义ActionBar布局 添加的Button用于保存设置壁纸
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
actionBar.setCustomView(R.layout.filtershow_actionbar);
View mSaveButton = actionBar.getCustomView();
mSaveButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
// 保存设置剪裁的壁纸
startFinishOutput();
}
});
}
if (intent.getData() != null) {
mSourceUri = intent.getData();
// 开始加载之前选择的图片
startLoadBitmap(mSourceUri);
} else {
// 否则回去选图片
pickImage();
}
}
private void enableSave(boolean enable) {
if (mSaveButton != null) {
mSaveButton.setEnabled(enable);
}
}
@Override
protected void onDestroy() {
// 取消加载Bitmap的任务
if (mLoadBitmapTask != null) {
mLoadBitmapTask.cancel(false);
}
super.onDestroy();
}
@Override
public void onConfigurationChanged (Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mCropView.configChanged();
}
/**
* Opens a selector in Gallery to chose an image for use when none was given
* in the CROP intent.
*/
private void pickImage() {
Intent intent = new Intent();
intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(Intent.createChooser(intent, getString(R.string.select_image)),
SELECT_PICTURE);
}
/**
* Callback for pickImage().
*/
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK && requestCode == SELECT_PICTURE) {
mSourceUri = data.getData();
startLoadBitmap(mSourceUri);
}
}
/**
* Gets screen size metric.
* 通过获取屏幕的宽和高 返回二者中的最大值 一般的竖屏手机肯定是返回高度了
*/
private int getScreenImageSize() {
DisplayMetrics outMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(outMetrics);
return (int) Math.max(outMetrics.heightPixels, outMetrics.widthPixels);
}
/**
* Method that loads a bitmap in an async task.
* 启动一个一步任务去加载Bitmap 这个Bitmap就是之前我们选择要设置为壁纸的图片
*/
private void startLoadBitmap(Uri uri) {
if (uri != null) {
enableSave(false);
final View loading = findViewById(R.id.loading);
loading.setVisibility(View.VISIBLE);
mLoadBitmapTask = new LoadBitmapTask();
mLoadBitmapTask.execute(uri);
} else {
cannotLoadImage();
done();
}
}
/**
* Method called on UI thread with loaded bitmap.
* 当加载完Bitmap之后将Bitmap赋值给mCropView 接着它负责图像的剪裁工作
*/
private void doneLoadBitmap(Bitmap bitmap, RectF bounds, int orientation) {
final View loading = findViewById(R.id.loading);
loading.setVisibility(View.GONE);
mOriginalBitmap = bitmap;
mOriginalBounds = bounds;
mOriginalRotation = orientation;
if (bitmap != null && bitmap.getWidth() != 0 && bitmap.getHeight() != 0) {
RectF imgBounds = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
mCropView.initialize(bitmap, imgBounds, imgBounds, orientation);
if (mCropExtras != null) {
int aspectX = mCropExtras.getAspectX();
int aspectY = mCropExtras.getAspectY();
mOutputX = mCropExtras.getOutputX();
mOutputY = mCropExtras.getOutputY();
if (mOutputX > 0 && mOutputY > 0) {
mCropView.applyAspect(mOutputX, mOutputY);
}
float spotX = mCropExtras.getSpotlightX();
float spotY = mCropExtras.getSpotlightY();
if (spotX > 0 && spotY > 0) {
mCropView.setWallpaperSpotlight(spotX, spotY);
}
if (aspectX > 0 && aspectY > 0) {
mCropView.applyAspect(aspectX, aspectY);
}
}
enableSave(true);
} else {
Log.w(LOGTAG, "could not load image for cropping");
cannotLoadImage();
setResult(RESULT_CANCELED, new Intent());
done();
}
}
/**
* Display toast for image loading failure.
* 无法加载图片
*/
private void cannotLoadImage() {
CharSequence text = getString(R.string.cannot_load_image);
Toast toast = Toast.makeText(this, text, Toast.LENGTH_SHORT);
toast.show();
}
/**
* AsyncTask for loading a bitmap into memory.
* 异步任务加载bitmap到内存
* @see #startLoadBitmap(Uri)
* @see #doneLoadBitmap(Bitmap)
*/
private class LoadBitmapTask extends AsyncTask<Uri, Void, Bitmap> {
int mBitmapSize;
Context mContext;
Rect mOriginalBounds;
int mOrientation;
public LoadBitmapTask() {
mBitmapSize = getScreenImageSize();
mContext = getApplicationContext();
mOriginalBounds = new Rect();
mOrientation = 0;
}
@Override
protected Bitmap doInBackground(Uri... params) {
Uri uri = params[0];
Bitmap bmap = ImageLoader.loadConstrainedBitmap(uri, mContext, mBitmapSize,
mOriginalBounds, false);
mOrientation = ImageLoader.getMetadataRotation(mContext, uri);
return bmap;
}
@Override
protected void onPostExecute(Bitmap result) {
doneLoadBitmap(result, new RectF(mOriginalBounds), mOrientation);
}
}
/**
*
*/
protected void startFinishOutput() {
if (finalIOGuard) {
return;
} else {
finalIOGuard = true;
}
enableSave(false);
Uri destinationUri = null;
int flags = 0;
if (mOriginalBitmap != null && mCropExtras != null) {
if (mCropExtras.getExtraOutput() != null) {
destinationUri = mCropExtras.getExtraOutput();
if (destinationUri != null) {
flags |= DO_EXTRA_OUTPUT;
}
}
if (mCropExtras.getSetAsWallpaper()) {
flags |= DO_SET_WALLPAPER;
}
if (mCropExtras.getReturnData()) {
flags |= DO_RETURN_DATA;
}
}
if (flags == 0) {
destinationUri = SaveImage.makeAndInsertUri(this, mSourceUri);
if (destinationUri != null) {
flags |= DO_EXTRA_OUTPUT;
}
}
if ((flags & FLAG_CHECK) != 0 && mOriginalBitmap != null) {
RectF photo = new RectF(0, 0, mOriginalBitmap.getWidth(), mOriginalBitmap.getHeight());
RectF crop = getBitmapCrop(photo);
startBitmapIO(flags, mOriginalBitmap, mSourceUri, destinationUri, crop,
photo, mOriginalBounds,
(mCropExtras == null) ? null : mCropExtras.getOutputFormat(), mOriginalRotation);
return;
}
setResult(RESULT_CANCELED, new Intent());
done();
return;
}
/**
* @category 开始BitmapIOTask任务
* @param flags
* @param currentBitmap
* @param sourceUri
* @param destUri
* @param cropBounds
* @param photoBounds
* @param currentBitmapBounds
* @param format
* @param rotation
*/
private void startBitmapIO(int flags, Bitmap currentBitmap, Uri sourceUri, Uri destUri,
RectF cropBounds, RectF photoBounds, RectF currentBitmapBounds, String format,
int rotation) {
if (cropBounds == null || photoBounds == null || currentBitmap == null
|| currentBitmap.getWidth() == 0 || currentBitmap.getHeight() == 0
|| cropBounds.width() == 0 || cropBounds.height() == 0 || photoBounds.width() == 0
|| photoBounds.height() == 0) {
return; // fail fast
}
if ((flags & FLAG_CHECK) == 0) {
return; // no output options
}
if ((flags & DO_SET_WALLPAPER) != 0) {
Toast.makeText(this, R.string.setting_wallpaper, Toast.LENGTH_LONG).show();
}
final View loading = findViewById(R.id.loading);
loading.setVisibility(View.VISIBLE);
BitmapIOTask ioTask = new BitmapIOTask(sourceUri, destUri, format, flags, cropBounds,
photoBounds, currentBitmapBounds, rotation, mOutputX, mOutputY);
ioTask.execute(currentBitmap);
}
/**
* 完成BitmapIO任务
*/
private void doneBitmapIO(boolean success, Intent intent) {
final View loading = findViewById(R.id.loading);
loading.setVisibility(View.GONE);
if (success) {
setResult(RESULT_OK, intent);
} else {
setResult(RESULT_CANCELED, intent);
}
done();
}
/**
* 对传递进来的Bimap进行处理,然后设置成壁纸
*/
private class BitmapIOTask extends AsyncTask<Bitmap, Void, Boolean> {
private final WallpaperManager mWPManager;
InputStream mInStream = null;
OutputStream mOutStream = null;
String mOutputFormat = null;
Uri mOutUri = null;
Uri mInUri = null;
int mFlags = 0;
RectF mCrop = null;
RectF mPhoto = null;
RectF mOrig = null;
Intent mResultIntent = null;
int mRotation = 0;
// Helper to setup input stream
private void regenerateInputStream() {
if (mInUri == null) {
Log.w(LOGTAG, "cannot read original file, no input URI given");
} else {
Utils.closeSilently(mInStream);
try {
mInStream = getContentResolver().openInputStream(mInUri);
} catch (FileNotFoundException e) {
Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
}
}
}
public BitmapIOTask(Uri sourceUri, Uri destUri, String outputFormat, int flags,
RectF cropBounds, RectF photoBounds, RectF originalBitmapBounds, int rotation,
int outputX, int outputY) {
mOutputFormat = outputFormat;
mOutStream = null;
mOutUri = destUri;
mInUri = sourceUri;
mFlags = flags;
mCrop = cropBounds;
mPhoto = photoBounds;
mOrig = originalBitmapBounds;
mWPManager = WallpaperManager.getInstance(getApplicationContext());
mResultIntent = new Intent();
mRotation = (rotation < 0) ? -rotation : rotation;
mRotation %= 360;
mRotation = 90 * (int) (mRotation / 90); // now mRotation is a multiple of 90
mOutputX = outputX;
mOutputY = outputY;
if ((flags & DO_EXTRA_OUTPUT) != 0) {
if (mOutUri == null) {
Log.w(LOGTAG, "cannot write file, no output URI given");
} else {
try {
mOutStream = getContentResolver().openOutputStream(mOutUri);
} catch (FileNotFoundException e) {
Log.w(LOGTAG, "cannot write file: " + mOutUri.toString(), e);
}
}
}
if ((flags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0) {
regenerateInputStream();
}
}
@Override
protected Boolean doInBackground(Bitmap... params) {
boolean failure = false;
Bitmap img = params[0];
// Set extra for crop bounds
if (mCrop != null && mPhoto != null && mOrig != null) {
RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig);
Matrix m = new Matrix();
m.setRotate(mRotation);
m.mapRect(trueCrop);
if (trueCrop != null) {
Rect rounded = new Rect();
trueCrop.roundOut(rounded);
mResultIntent.putExtra(CropExtras.KEY_CROPPED_RECT, rounded);
}
}
// Find the small cropped bitmap that is returned in the intent
if ((mFlags & DO_RETURN_DATA) != 0) {
assert (img != null);
Bitmap ret = getCroppedImage(img, mCrop, mPhoto);
if (ret != null) {
ret = getDownsampledBitmap(ret, MAX_BMAP_IN_INTENT);
}
if (ret == null) {
Log.w(LOGTAG, "could not downsample bitmap to return in data");
failure = true;
} else {
if (mRotation > 0) {
Matrix m = new Matrix();
m.setRotate(mRotation);
Bitmap tmp = Bitmap.createBitmap(ret, 0, 0, ret.getWidth(),
ret.getHeight(), m, true);
if (tmp != null) {
ret = tmp;
}
}
mResultIntent.putExtra(CropExtras.KEY_DATA, ret);
}
}
// Do the large cropped bitmap and/or set the wallpaper
if ((mFlags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0 && mInStream != null) {
// Find crop bounds (scaled to original image size)
RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig);
if (trueCrop == null) {
Log.w(LOGTAG, "cannot find crop for full size image");
failure = true;
return false;
}
Rect roundedTrueCrop = new Rect();
trueCrop.roundOut(roundedTrueCrop);
if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
Log.w(LOGTAG, "crop has bad values for full size image");
failure = true;
return false;
}
// Attempt to open a region decoder
BitmapRegionDecoder decoder = null;
try {
decoder = BitmapRegionDecoder.newInstance(mInStream, true);
} catch (IOException e) {
Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
}
Bitmap crop = null;
if (decoder != null) {
// Do region decoding to get crop bitmap
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
crop = decoder.decodeRegion(roundedTrueCrop, options);
decoder.recycle();
}
if (crop == null) {
// BitmapRegionDecoder has failed, try to crop in-memory
regenerateInputStream();
Bitmap fullSize = null;
if (mInStream != null) {
fullSize = BitmapFactory.decodeStream(mInStream);
}
if (fullSize != null) {
crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
roundedTrueCrop.top, roundedTrueCrop.width(),
roundedTrueCrop.height());
}
}
if (crop == null) {
Log.w(LOGTAG, "cannot decode file: " + mInUri.toString());
failure = true;
return false;
}
if (mOutputX > 0 && mOutputY > 0) {
Matrix m = new Matrix();
RectF cropRect = new RectF(0, 0, crop.getWidth(), crop.getHeight());
if (mRotation > 0) {
m.setRotate(mRotation);
m.mapRect(cropRect);
}
RectF returnRect = new RectF(0, 0, mOutputX, mOutputY);
m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
m.preRotate(mRotation);
Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
(int) returnRect.height(), Bitmap.Config.ARGB_8888);
if (tmp != null) {
Canvas c = new Canvas(tmp);
c.drawBitmap(crop, m, new Paint());
crop = tmp;
}
} else if (mRotation > 0) {
Matrix m = new Matrix();
m.setRotate(mRotation);
Bitmap tmp = Bitmap.createBitmap(crop, 0, 0, crop.getWidth(),
crop.getHeight(), m, true);
if (tmp != null) {
crop = tmp;
}
}
// Get output compression format
CompressFormat cf =
convertExtensionToCompressFormat(getFileExtension(mOutputFormat));
// If we only need to output to a URI, compress straight to file
if (mFlags == DO_EXTRA_OUTPUT) {
if (mOutStream == null
|| !crop.compress(cf, DEFAULT_COMPRESS_QUALITY, mOutStream)) {
Log.w(LOGTAG, "failed to compress bitmap to file: " + mOutUri.toString());
failure = true;
} else {
mResultIntent.setData(mOutUri);
}
} else {
// Compress to byte array
ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
// If we need to output to a Uri, write compressed
// bitmap out
if ((mFlags & DO_EXTRA_OUTPUT) != 0) {
if (mOutStream == null) {
Log.w(LOGTAG,
"failed to compress bitmap to file: " + mOutUri.toString());
failure = true;
} else {
try {
mOutStream.write(tmpOut.toByteArray());
mResultIntent.setData(mOutUri);
} catch (IOException e) {
Log.w(LOGTAG,
"failed to compress bitmap to file: "
+ mOutUri.toString(), e);
failure = true;
}
}
}
// If we need to set to the wallpaper, set it
if ((mFlags & DO_SET_WALLPAPER) != 0 && mWPManager != null) {
if (mWPManager == null) {
Log.w(LOGTAG, "no wallpaper manager");
failure = true;
} else {
try {
mWPManager.setStream(new ByteArrayInputStream(tmpOut
.toByteArray()));
} catch (IOException e) {
Log.w(LOGTAG, "cannot write stream to wallpaper", e);
failure = true;
}
}
}
} else {
Log.w(LOGTAG, "cannot compress bitmap");
failure = true;
}
}
}
return !failure; // True if any of the operations failed
}
@Override
protected void onPostExecute(Boolean result) {
Utils.closeSilently(mOutStream);
Utils.closeSilently(mInStream);
doneBitmapIO(result.booleanValue(), mResultIntent);
}
}
private void done() {
finish();
}
/**
* @category 返回CroppedImage
* @param image
* @param cropBounds
* @param photoBounds
* @return
*/
protected static Bitmap getCroppedImage(Bitmap image, RectF cropBounds, RectF photoBounds) {
RectF imageBounds = new RectF(0, 0, image.getWidth(), image.getHeight());
RectF crop = CropMath.getScaledCropBounds(cropBounds, photoBounds, imageBounds);
if (crop == null) {
return null;
}
Rect intCrop = new Rect();
crop.roundOut(intCrop);
return Bitmap.createBitmap(image, intCrop.left, intCrop.top, intCrop.width(),
intCrop.height());
}
protected static Bitmap getDownsampledBitmap(Bitmap image, int max_size) {
if (image == null || image.getWidth() == 0 || image.getHeight() == 0 || max_size < 16) {
throw new IllegalArgumentException("Bad argument to getDownsampledBitmap()");
}
int shifts = 0;
int size = CropMath.getBitmapSize(image);
while (size > max_size) {
shifts++;
size /= 4;
}
Bitmap ret = Bitmap.createScaledBitmap(image, image.getWidth() >> shifts,
image.getHeight() >> shifts, true);
if (ret == null) {
return null;
}
// Handle edge case for rounding.
if (CropMath.getBitmapSize(ret) > max_size) {
return Bitmap.createScaledBitmap(ret, ret.getWidth() >> 1, ret.getHeight() >> 1, true);
}
return ret;
}
/**
* Gets the crop extras from the intent, or null if none exist.
*/
protected static CropExtras getExtrasFromIntent(Intent intent) {
Bundle extras = intent.getExtras();
if (extras != null) {
return new CropExtras(extras.getInt(CropExtras.KEY_OUTPUT_X, 0),
extras.getInt(CropExtras.KEY_OUTPUT_Y, 0),
extras.getBoolean(CropExtras.KEY_SCALE, true) &&
extras.getBoolean(CropExtras.KEY_SCALE_UP_IF_NEEDED, false),
extras.getInt(CropExtras.KEY_ASPECT_X, 0),
extras.getInt(CropExtras.KEY_ASPECT_Y, 0),
extras.getBoolean(CropExtras.KEY_SET_AS_WALLPAPER, false),
extras.getBoolean(CropExtras.KEY_RETURN_DATA, false),
(Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT),
extras.getString(CropExtras.KEY_OUTPUT_FORMAT),
extras.getBoolean(CropExtras.KEY_SHOW_WHEN_LOCKED, false),
extras.getFloat(CropExtras.KEY_SPOTLIGHT_X),
extras.getFloat(CropExtras.KEY_SPOTLIGHT_Y));
}
return null;
}
protected static CompressFormat convertExtensionToCompressFormat(String extension) {
return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
}
protected static String getFileExtension(String requestFormat) {
String outputFormat = (requestFormat == null)
? "jpg"
: requestFormat;
outputFormat = outputFormat.toLowerCase();
return (outputFormat.equals("png") || outputFormat.equals("gif"))
? "png" // We don't support gif compression.
: "jpg";
}
private RectF getBitmapCrop(RectF imageBounds) {
RectF crop = mCropView.getCrop();
RectF photo = mCropView.getPhoto();
if (crop == null || photo == null) {
Log.w(LOGTAG, "could not get crop");
return null;
}
RectF scaledCrop = CropMath.getScaledCropBounds(crop, photo, imageBounds);
return scaledCrop;
}
}
CropActivity的处理逻辑是这样,首先开启一个异步任务根据
传递过来的信息
去加载壁纸Bitmap。加载完毕再开启一个异步任务去处理这个Bitmap,处理完最后设置为壁纸。代码已经添加了注释。
设置完毕返回到桌面,会有1到2秒的设置时间,之后壁纸就设置好了。设置好的壁纸是可以随着桌面滑动的,这么说不太准确,应该说是Launcher让壁纸随着桌面滑动而滑动(parallax effects)。处理逻辑是在Workspace类里,由于Workspace类代码较长就不贴代码了,找相关代码分析。
变量:
private float mWallpaperScrollRatio = 1.0f;
private int mOriginalPageSpacing ;
private final WallpaperManager mWallpaperManager;
private IBinder mWindowToken;
private static final float WALLPAPER_SCREENS_SPAN = 2f;
enum WallpaperVerticalOffset {
TOP, MIDDLE , BOTTOM
};
int mWallpaperWidth;
int mWallpaperHeight;
WallpaperOffsetInterpolator mWallpaperOffset;
boolean mUpdateWallpaperOffsetImmediately = false ;
private Runnable mDelayedResizeRunnable;
private Runnable mDelayedSnapToPageRunnable;
private Point mDisplaySize = new Point();
private boolean mIsStaticWallpaper ;
private int mWallpaperTravelWidth ;
private int mSpringLoadedPageSpacing ;
private int mCameraDistance ;
mWallpaperManager = WallpaperManager.getInstance(context);
mWallpaperOffset = new WallpaperOffsetInterpolator();
mWallpaperTravelWidth = (int) (mDisplaySize. x * wallpaperTravelToScreenWidthRatio(mDisplaySize.x , mDisplaySize .y ));
mIsStaticWallpaper = mWallpaperManager.getWallpaperInfo() == null ;
方法和类:
setWallpaperDimension()
wallpaperTravelToScreenWidthRatio()
wallpaperOffsetForCurrentScroll()
syncWallpaperOffsetWithScroll()
updateWallpaperOffsetImmediately()
updateWallpaperOffsets()
computeWallpaperScrollRatio()
WallpaperOffsetInterpolator类
可以说是WallpaperOffsetInterpolator负责壁纸滑动的,其他变量和方法为它服务。setWallpaperDimension()方法是设置壁纸的大小,这个方法在Launcher类初始化时候会调用。wallpaperOffsetForCurrentScroll()方法用于计算滑动壁纸需要移动的距离,它在syncWallpaperOffsetWithScroll()方法中调用,syncWallpaperOffsetWithScroll()方法很简单,如果手机开启硬件加速就执行WallpaperOffsetInterpolator的setFinalX方法,setFinalX方法再去调用wallpaperOffsetForCurrentScroll()方法。
private void syncWallpaperOffsetWithScroll() {
final boolean enableWallpaperEffects = isHardwareAccelerated();
if (enableWallpaperEffects) {
mWallpaperOffset.setFinalX(wallpaperOffsetForCurrentScroll());
}
}
如果我们把
enableWallpaperEffects值改为false会怎么样呢,没错就是固定不动的壁纸了。updateWallpaperOffsets()方法用于在ondraw
()方法中去更新视图(滑动的时候),配合在computeScroll()方法中调用
syncWallpaperOffsetWithScroll()方法。
@Override
public void computeScroll() {
super .computeScroll();
syncWallpaperOffsetWithScroll();
}
关于Launcher设置壁纸的流程就先介绍到这里,有疏漏的地方还请指正。
有功能就会有Bug,那么一般关于壁纸常见的Bug有哪些呢?下一篇文章会具体介绍,有这样的经验的朋友欢迎一切探讨、交流。