Android Launcher 专属页和生活页的实现(一)

心想才能事成!
成功只会降临在那些自我感觉会成功的人的身上。 —-拿破仑·希尔

概述

在讲解之前,我们先来看一张成品图吧。

Launcher0

Launcher1

是不是很漂亮啊!那两个音乐页和相机页是怎么加上去的呢?是如何实现跨进程显示view的? 目前我知道两种方式:
1. RemoteViews
    RemoteViews描述的是一个跨进程显示的view。
    RemoteViews使用最多的场合是通知栏和桌面小插件
    但是RemoteView并不支持所有的View:
    布局:FrameLayout、LinearLayout、RelativeLayout、GridLayout 
    组件:Button、ImageButton、ImageView、TextView、ListView、GridView、ViewStub等

2.  通过Context的createPackageContext方法,获取其它包的上下文Context,然后获取其他包的布局view。

由于RemoteView并不支持所有的View,所以我们这个实例采用的是借壳方式。所谓借壳就是采用AppWidget结构,用createPackageContext 获取的上下文来加载的其他包的view来替代RemoteView,下面就开始做详细的介绍吧,下面就称为“专属widget”。

准备工作

  • 准备音乐和相机两个widget应用。因为这两个应用要独立拿出来管理,所以在AndroidManifest.xml中添加信息:
    <intent-filter>
    <action android:name="android.appwidget.action.PRIVATE_PAGE" />
    </intent-filter>

    用来检索这两个widget。同时这两个widget还要包含一些各自的信息,所以要在AndroidManifest.xml中添加要表达的信息:
    <meta-data
    android:name="dqf.private.page.info"
    android:resource="@xml/private_info" />

    方便在Launcher中loadXmlMetaData获取相关信息。
  • 对Launcher架构要熟悉,这两个widget要和Launcher中其他的widget分开,在Launcher中创建widget的LauncherAppWidgetInfo的时候,用appWidgetId来区分,普通的widget的appWidgetId由AppWidgetHost.allocateAppWidgetId()申请获得或者由AppWidgetHostView.getAppWidgetId()获得。而这两个特殊的widget由我们自己制定一个特殊的值,比如:-110。

解析和加载专属widget

系统层解析加载一个widget主要的类有:
1、AppWidgetHostView
AppWidgetHostView是一个view,是容纳APP传来的View的容器。而RemoteViews就是用来传递APP的View的。
2、AppWidgetHos
3、AppWidgetProvider
4、AppWidgetServiceImpl
5、AppWidgetManager
6、AppWidgetProviderInfo

在Launcher上显示widget,我们需要关心的是AppWidgetHostView和AppWidgetProviderInfo两个文件。

因为我们要独立出来两个特殊的widget,所以在这两个Widget的AndroidManifest.xml的AppWidgetProvider生命中不添加
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>

这样的话,AppWidgetServiceImpl系统中就不会加载到这两个widget了。
我们需要单独定义一个action,如下:
<intent-filter>
<action android:name="android.appwidget.action.PRIVATE_PAGE" />
</intent-filter>

第一步:解析专属widget的信息:

1. workspace部分的解析:
    workspace直接显示这两个widget,是最主要的部分。
       synchronized (mPrivateWidgets) {
                mPrivateWidgets.clear();
                //在上文中,我提到要独立赛选出这两个widget,就用"android.appwidget.action.PRIVATE_PAGE"
                List privatewidgets = mPackageManager.queryBroadcastReceivers(new Intent(
                    "android.appwidget.action.PRIVATE_PAGE"), PackageManager.GET_META_DATA);

                if(privatewidgets != null && privatewidgets.size() > 0) {
                    for(int i = 0; i < privatewidgets.size(); i++){
                        try {
                            ResolveInfo info = (ResolveInfo) privatewidgets.get(i);
                            ComponentName componentname = new ComponentName(
                                    info.activityInfo.applicationInfo.packageName,
                                    info.activityInfo.name);
                            //创建DqfAppWidgetProviderInfo对象,该类中包含了widget的相关信息
                            DqfAppWidgetProviderInfo dqfappwidgetproviderinfo = new DqfAppWidgetProviderInfo (
                                    mLauncher, componentname);
                            mPrivateWidgets.add(dqfappwidgetproviderinfo );
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
因为我们的widget不再走系统的那套流程,所以我们要建一个AppWidgetProviderInfo的子类,需要解析这两个widget的信息。
所以我们定义了一个类DqfAppWidgetProviderInfo,继承AppWidgetProviderInfo。在类的构造函数中解析指定ComponentName的widget信息。
和AppWidgetServiceImpl 的 parseProviderInfoXml的解析是一样的,需要了解的可以去看一下这个函数,
就是用来解析Widget应用的AndroidManifest.xml中的

<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />

解析xml文件widget_info。从widget_info.xml中,我们要获取
1、minWidth 和minHeight:App Widget布局需要的最小区域
2、updatePeriodMillis:widget 的更新频率
3、initialLayout:指向 widget 的布局资源文件
4、previewImage:指定预览图,该预览图在用户选择 widget 时出现。

2.  Hotseat和Indicator部分的解析:
如图:

dockbar
两个专属widget,hotseat部分分别对应不同的应用。page_indicator部分也对应了不同的小角标。这些信息都来自专属widget的AndroidManifest.xml中的
<meta-data
android:name="dqf.private.page.info"
android:resource="@xml/private_info" />

private_info.xml:
<?xml version="1.0" encoding="utf-8"?>
<privatepage xmlns:privatepage="http://schemas.android.com/apk/res/com.dqf.moodalbum"
privatepage:dockIntent1="@string/dockIntent1"
privatepage:dockIntent2="@string/dockIntent2"
privatepage:dockIntent3="@string/dockIntent3"
privatepage:pointIndicator="@drawable/private_icon"
privatepage:pointIndicatorFocus="@drawable/private_icon_focus" />

解析private_info.xml文件,同样需要定义一个类来管理这些信息PrivatePageInfo。解析:

public PrivatePageInfo(Context context, ApplicationInfo application)
            throws XmlPullParserException, IOException {
        mApplicationInfo = application;
        mPm = context.getPackageManager();
        XmlResourceParser parser = null;
        try {
                parser = application.loadXmlMetaData(mPm, "dqf.private.page.info");
            }

            if (parser == null) {
                throw new XmlPullParserException(
                        "No dqf.private.page.info meta-data");
            }

            int type;
            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                    && type != XmlPullParser.START_TAG) {
            }

            String nodeName = parser.getName();
            Log.d(TAG, "nodeName =" + nodeName);
            if (!"privatepage".equals(nodeName)) {
                throw new XmlPullParserException(
                        "Meta-data does not start with LockInfo tag");
            }

            String namespace = parser.getAttributeNamespace(0);

            //mPointIndicatorId 和mPointIndicatorFocusId是page_indicator部分对应的小图标的资源id。然后再通过BitmapFactory.decodeResource(
                            localResources, mPointIndicatorId)获取图片资源(localResources是专属widget应用的上下文资源哦)。
            mPointIndicatorId = parser.getAttributeResourceValue(namespace, "pointIndicator", -1);
            mPointIndicatorFocusId = parser.getAttributeResourceValue(namespace, "pointIndicatorFocus", -1);

//mDockIntent1 mDockIntent2 mDockIntent3是hotseat部分对应的三个应用的信息的ID,然后再通过mPm.getText(mApplicationInfo.packageName, mDockIntent1,mApplicationInfo)获取具体的信息(是三个String,信息内容是ComponentName)。
            mDockIntent1 = parser.getAttributeResourceValue(namespace, "dockIntent1", -1);
            mDockIntent2 = parser.getAttributeResourceValue(namespace, "dockIntent2", -1);
            mDockIntent3 = parser.getAttributeResourceValue(namespace, "dockIntent3", -1);
            if (parser != null) {
                parser.close();
            }

        } catch (Exception localException) {
            throw new XmlPullParserException("Unable to create context for: "
                    + mApplicationInfo.packageName);
        } finally {
            if (parser != null) {
                parser.close();
            }
        }
    }

第二步:加载专属widget到Launcher界面:

  1. 专属widget预览界面的显示
    如图:
    editor
    这两个图片就是widget的预览图,就是上文我们解析出来的previewImage。在Adapter中加载,在布局中显示出来。

  2. 专属widget在workspace的显示
    workspace显示的是AppWidgetHostView中通过RemoteViews传递过来的widget的view。
    Launcher中定义了LauncherAppWidgetHostView类,继承AppWidgetHostView。
    然后使用方法:
    launcherInfo.hostView = (LauncherAppWidgetHostView)mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo);
    launcherInfo.hostView.setAppWidget(appWidgetId, appWidgetInfo);

    哈哈哈!
    其实这里的createView是获取不到RemoteView的,因为专属widget没有加载到系统里(上文已经分析了过了),并且专属widget的appWidgetId不是系统allocate的,是我们自己定义的-110,所以AppWidgetServiceImpl找不到ID为-110对应的RemoteViews。
    怎么办?? 方法:
    View view = ((LayoutInflater) mContext.getSystemService( Context.LAYOUT_INFLATER_SERVICE)).inflate(resource, parent);
    mInfo.hostView.addView(view);
    mInfo.hostView.setChildView(view);

    OK,借壳成功。
    然后就是Launcher的流程了:
    mWorkspace.addInScreen(launcherInfo.hostView, container, screenId, cellXY[0], cellXY[1],
    launcherInfo.spanX, launcherInfo.spanY, isWorkspaceLocked());

    addInScreen到桌面上。这是显示这一部分的主要代码,中间的一些细节,就不介绍了。还有专属widget的删除和添加等等。都不难,只要你对Launcher的流程很熟悉,都不在话下。

  3. Hotseat部分的显示
    如图:
    dockbar

    专属widget的DockBar部分创建好之后,下面就开始解决滑动页面,DockBar跟着变化的问题了。因为专属widget有两个,可能还有更多,所以要建立一个类PrivatePageDockBarManager来管理专属widget的DockBar部分,这个类的主要功能是添加、删除、获取、查询DockBar。

    DockBar如何添加到桌面,采用Launcher的DragLayer来addView到Launcher的底部。

    那么如何让不同专属widget对应的DockBar跟着页面的滑动变化呢?在Launcher的onCreate中,给Workspace注册一个PageSwitchListener的监听事件:

mWorkspace.setPageSwitchListener(new PagedView.PageSwitchListener() {
    public void onPageSwitch(View view, int i) {
        changeHotseatVisibility(i);
    }
});

在changeHotseatVisibility函数中,写DockBar的切换显示。

  1. Indicator部分图标的跟换
    这部分就更简单了,随着页面的滑动,更新专属widget的indicator指示图就可以了。PageIndicator类中做好更改。

结束语

发现写技术博客好困难,之前也没写过。只懂得如果写代码,表达出来还真是困难。但是,在写博客的过程中,真的有很大收获,因为写博客的过程就是对专业知识的巩固,对开发功能的再次审核,对开发功能的细节再次思考。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值