Android Launcher 设置壁纸

转自: WPJY 的博客  

一、概述

 一般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打开一个界面选择具备设置壁纸的应用。

 每一个应用都有自己设置壁纸的功能,而且也不尽相同。360是这样:

 原生图库是这样:

那下面就以原生图库为例来梳理一下这个流程,还是以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的代码健壮性都还不错,不是一般的强。项目工程大体如下:

 当然大多数还是负责处理显示图片、编辑图片的功能,对于设置壁纸的功能代码不是很多。那么当从选择的界面中选择图库这个应用后,流程是怎么样的呢?之前是发送了一个

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有哪些呢?下一篇文章会具体介绍,有这样的经验的朋友欢迎一切探讨、交流。

花絮:

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

依赖类库:

https://code.google.com/p/mp4parser/

图像剪裁开源项目:

1、https://github.com/edmodo/cropper(比较完善)

2、https://github.com/jdamcd/android-crop

3、https://github.com/biokys/cropimage

4、https://github.com/lvillani/android-cropimage