本文主要介绍launcher3中Hotseat的实现,同时参照go launcher的界面,给出了一个demo.
1、 hotseat的界面
在launcher3中,其主界面如下图所示,其中用红色圈标注了其中的hotseat(如下图左):
本文给出了一个改装版本,图上图右所示。这样是目前市面上大多数launcher产品实现的方案。当然,部分产品,还将这5个图标中的最右边一个设置为可以自动更改,比如说,你可以将右边那个浏览器图标改为任意app图标。
2、launcher3中hotseat的实现
我们来看一下他们的layout文件:
res/layout-port/launcher.xml:
<RelativeLayout
android:id="@+id/all_apps_button_cluster"
android:layout_width="fill_parent"
android:layout_height="@dimen/button_bar_height"
android:layout_gravity="bottom|center_horizontal"
android:paddingTop="2dip"
>
<com.android.launcher3.HandleView
style="@style/HotseatButton"
android:id="@+id/all_apps_button"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:src="@drawable/all_apps_button"
launcher:direction="horizontal"
/>
<ImageView
android:id="@+id/hotseat_left"
style="@style/HotseatButton.Left"
android:layout_toLeftOf="@id/all_apps_button"
android:src="@drawable/hotseat_phone"
android:onClick="launchHotSeat"
/>
<ImageView
android:id="@+id/hotseat_right"
style="@style/HotseatButton.Right"
android:layout_toRightOf="@id/all_apps_button"
android:src="@drawable/hotseat_browser"
android:onClick="launchHotSeat"
/>
</RelativeLayout>
其中,你可以看到,一个打电话图标和一个浏览器图标,一个app list的图标。
2.1 点击时的状态变化
关于这一点,你在drawable文件下,可以看到hotseat_phone.xml,all_apps_button.xml,hotseat_browser.xml这三个按钮的状态信息,其中包括焦点在按钮上、点击等状态,以hotseat_phone.xml内容为例:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/hotseat_phone_pressed" />
<item android:state_focused="true" android:state_window_focused="true" android:drawable="@drawable/hotseat_phone_focused" />
<item android:state_focused="true" android:state_window_focused="false" android:drawable="@drawable/hotseat_phone_normal" />
<item android:drawable="@drawable/hotseat_phone_normal" />
</selector>
2.2 按钮背景及位置
关于这一点,可以看到hotseat_phone位于app list的左边,其中由如下属性定义:
android:layout_toLeftOf="@id/all_apps_button"
同时在style文件中,还定义其背景以及间隔信息:
<style name="HotseatButton">
<item name="android:paddingLeft">12dip</item>
<item name="android:paddingRight">12dip</item>
<item name="android:background">@drawable/hotseat_bg_center</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">fill_parent</item>
<item name="android:scaleType">center</item>
<item name="android:focusable">true</item>
<item name="android:clickable">true</item>
</style>
<style name="HotseatButton.Left">
<item name="android:layout_marginLeft">4dip</item>
<item name="android:background">@drawable/hotseat_bg_left</item>
</style>
<style name="HotseatButton.Right">
<item name="android:layout_marginRight">4dip</item>
<item name="android:background">@drawable/hotseat_bg_right</item>
</style>
2.3 初始化
在launcher.java文件中,对其进行初始化,主要包括三个函数:
setupViews();//初始化各个view
loadHotseats();//初始化打电话和搜索那两个按钮的intent
launchHotSeat(View v)//实际上就是点击了hotseat之后,转入对应的程序而已
其中代码分析如下:
setupViews();//初始化各个view
mHandleView = (HandleView) findViewById(R.id.all_apps_button);
mHandleView.setLauncher(this);
mHandleView.setOnClickListener(this);
mHandleView.setOnLongClickListener(this);
ImageView hotseatLeft = (ImageView) findViewById(R.id.hotseat_left);
hotseatLeft.setContentDescription(mHotseatLabels[0]);
hotseatLeft.setImageDrawable(mHotseatIcons[0]);
ImageView hotseatRight = (ImageView) findViewById(R.id.hotseat_right);
hotseatRight.setContentDescription(mHotseatLabels[1]);
hotseatRight.setImageDrawable(mHotseatIcons[1]);
loadHotseats();//初始化打电话和搜索那两个按钮的intent
// Load the Intent templates from arrays.xml to populate the hotseats. For
// each Intent, if it resolves to a single app, use that as the launch
// intent & use that app's label as the contentDescription. Otherwise,
// retain the ResolveActivity so the user can pick an app.
private void loadHotseats() {
//获得arrays.xml文件中,定义的信息
if (mHotseatConfig == null) {
mHotseatConfig = getResources().getStringArray(R.array.hotseats);
if (mHotseatConfig.length > 0) {
mHotseats = new Intent[mHotseatConfig.length];
mHotseatLabels = new CharSequence[mHotseatConfig.length];
mHotseatIcons = new Drawable[mHotseatConfig.length];
} else {
mHotseats = null;
mHotseatIcons = null;
mHotseatLabels = null;
}
TypedArray hotseatIconDrawables = getResources().obtainTypedArray(R.array.hotseat_icons);
for (int i=0; i<mHotseatConfig.length; i++) {
// load icon for this slot; currently unrelated to the actual activity
try {
mHotseatIcons[i] = hotseatIconDrawables.getDrawable(i);
} catch (ArrayIndexOutOfBoundsException ex) {
Log.w(TAG, "Missing hotseat_icons array item #" + i);
mHotseatIcons[i] = null;
}
}
hotseatIconDrawables.recycle();
}
//根据预先定义的信息,获得这些按钮所对应的intent信息
PackageManager pm = getPackageManager();
for (int i=0; i<mHotseatConfig.length; i++) {
Intent intent = null;
if (mHotseatConfig[i].equals("*BROWSER*")) {
// magic value meaning "launch user's default web browser"
// replace it with a generic web request so we can see if there is indeed a default
String defaultUri = getString(R.string.default_browser_url);
intent = new Intent(
Intent.ACTION_VIEW,
((defaultUri != null)
? Uri.parse(defaultUri)
: getDefaultBrowserUri())
).addCategory(Intent.CATEGORY_BROWSABLE);
// note: if the user launches this without a default set, she
// will always be taken to the default URL above; this is
// unavoidable as we must specify a valid URL in order for the
// chooser to appear, and once the user selects something, that
// URL is unavoidably sent to the chosen app.
} else {
try {
intent = Intent.parseUri(mHotseatConfig[i], 0);
} catch (java.net.URISyntaxException ex) {
Log.w(TAG, "Invalid hotseat intent: " + mHotseatConfig[i]);
// bogus; leave intent=null
}
}
if (intent == null) {
mHotseats[i] = null;
mHotseatLabels[i] = getText(R.string.activity_not_found);
continue;
}
if (LOGD) {
Log.d(TAG, "loadHotseats: hotseat " + i
+ " initial intent=["
+ intent.toUri(Intent.URI_INTENT_SCHEME)
+ "]");
}
//根据intent信息,获得最匹配的app,这样点击该按钮后就进入到最匹配的app
ResolveInfo bestMatch = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
List<ResolveInfo> allMatches = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
if (LOGD) {
Log.d(TAG, "Best match for intent: " + bestMatch);
Log.d(TAG, "All matches: ");
for (ResolveInfo ri : allMatches) {
Log.d(TAG, " --> " + ri);
}
}
// did this resolve to a single app, or the resolver?
if (allMatches.size() == 0 || bestMatch == null) {
// can't find any activity to handle this. let's leave the
// intent as-is and let Launcher show a toast when it fails
// to launch.
mHotseats[i] = intent;
// set accessibility text to "Not installed"
mHotseatLabels[i] = getText(R.string.activity_not_found);
} else {
boolean found = false;
for (ResolveInfo ri : allMatches) {
if (bestMatch.activityInfo.name.equals(ri.activityInfo.name)
&& bestMatch.activityInfo.applicationInfo.packageName
.equals(ri.activityInfo.applicationInfo.packageName)) {
found = true;
break;
}
}
if (!found) {
if (LOGD) Log.d(TAG, "Multiple options, no default yet");
// the bestMatch is probably the ResolveActivity, meaning the
// user has not yet selected a default
// so: we'll keep the original intent for now
mHotseats[i] = intent;
// set the accessibility text to "Select shortcut"
mHotseatLabels[i] = getText(R.string.title_select_shortcut);
} else {
// we have an app!
// now reconstruct the intent to launch it through the front
// door
ComponentName com = new ComponentName(
bestMatch.activityInfo.applicationInfo.packageName,
bestMatch.activityInfo.name);
mHotseats[i] = new Intent(Intent.ACTION_MAIN).setComponent(com);
// load the app label for accessibility
mHotseatLabels[i] = bestMatch.activityInfo.loadLabel(pm);
}
}
if (LOGD) {
Log.d(TAG, "loadHotseats: hotseat " + i
+ " final intent=["
+ ((mHotseats[i] == null)
? "null"
: mHotseats[i].toUri(Intent.URI_INTENT_SCHEME))
+ "] label=[" + mHotseatLabels[i]
+ "]"
);
}
}
}
在arrays.xml中的定义如下:
<string-array name="hotseats" translatable="false">
<item>intent:#Intent;action=android.intent.action.DIAL;end</item>
<item>*BROWSER*</item>
</string-array>
<array name="hotseat_icons" translatable="false">
<item>@drawable/hotseat_phone</item>
<item>@drawable/hotseat_browser</item>
</array>
launchHotSeat(View v)//实际上就是点击了hotseat之后,转入对应的程序而已
//实际上就是点击了hotseat之后,转入对应的程序而已
@SuppressWarnings({"UnusedDeclaration"})
public void launchHotSeat(View v) {
if (isAllAppsVisible()) return;
int index = -1;
if (v.getId() == R.id.hotseat_left) {
index = 0;
} else if (v.getId() == R.id.hotseat_right) {
index = 1;
}
// reload these every tap; you never know when they might change
loadHotseats();
if (index >= 0 && index < mHotseats.length && mHotseats[index] != null) {
Intent intent = mHotseats[index];
startActivitySafely(
mHotseats[index],
"hotseat"
);
}
}
这个函数实际上就是根据所点击的view的id,进入到对应的app中,这些对应的app是在loadHotseats函数中就已经匹配好的。需要注意的是在这个函数也调用了一次loadHotseats函数,这个主要是因为第一次调用是在launcher初始化阶段,但是初始化以后,有可能系统以及被改变(比如原来最匹配的app被删除掉等等),这时候就需要重新进行配置。
3、 对hotseat的优化
在上面的图中,我们对hotseat进行了优化,将其中的按钮从三个扩充到了五个。这个具体是怎么实现的呢?
从第二部分的分析,我们已经知道了跟hotseat有关的部分,因此我们只要将这五个部分做个修改不久可以了么?
3.1launcher.xml
<RelativeLayout
android:id="@+id/all_apps_button_cluster"
android:layout_width="fill_parent"
android:layout_height="@dimen/button_bar_height"
android:layout_gravity="bottom|center_horizontal"
android:paddingTop="2dip"
>
<ImageView
android:id="@+id/hotseat_left0"
style="@style/HotseatButton.Left"
android:layout_toLeftOf="@+id/hotseat_left1"
android:src="@drawable/hotseat_phone"
android:onClick="launchHotSeat"
/>
<ImageView
android:id="@+id/hotseat_left1"
style="@style/HotseatButton.center"
android:layout_toLeftOf="@id/all_apps_button"
android:src="@drawable/hotseat_contact"
android:onClick="launchHotSeat"
/>
<com.xuxm.demo.launcher.HandleView
style="@style/HotseatButton"
android:id="@+id/all_apps_button"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:src="@drawable/all_apps_button"
launcher:direction="horizontal"
/>
<ImageView
android:id="@+id/hotseat_right1"
style="@style/HotseatButton.center"
android:layout_toRightOf="@id/all_apps_button"
android:src="@drawable/hotseat_message"
android:onClick="launchHotSeat"
/>
<ImageView
android:id="@+id/hotseat_right0"
style="@style/HotseatButton.Right"
android:layout_toRightOf="@id/hotseat_right1"
android:src="@drawable/hotseat_browser"
android:onClick="launchHotSeat"
/>
</RelativeLayout>
3.2 drawable
添加2个按钮文件:hotseat_contact.xml,hotseat_message.xml。
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/hotseat_contact_pressed" />
<item android:state_focused="true" android:state_window_focused="true" android:drawable="@drawable/hotseat_contact_focused" />
<item android:state_focused="true" android:state_window_focused="false" android:drawable="@drawable/hotseat_contact_normal" />
<item android:drawable="@drawable/hotseat_contact_normal" />
</selector>
这里对应的,你需要添加三张图片hotseat_contact_normal.png,hotseat_contact_pressed.png,hotseat_contact_focused.png。
3.3arrays.xml
<string-array name="hotseats" translatable="false">
<item>intent:#Intent;action=android.intent.action.DIAL;end</item>
<item>*CONTACT* </item>
<item>*MESSAGE* </item>
<item>*BROWSER*</item>
</string-array>
<array name="hotseat_icons" translatable="false">
<item>@drawable/hotseat_phone</item>
<item>@drawable/hotseat_contact</item>
<item>@drawable/hotseat_message</item>
<item>@drawable/hotseat_browser</item>
</array>
3.4styles.xml
<style name="HotseatButton">
<item name="android:paddingLeft">12dip</item>
<item name="android:paddingRight">12dip</item>
<item name="android:background">@drawable/hotseat_bg_center</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">fill_parent</item>
<item name="android:scaleType">center</item>
<item name="android:focusable">true</item>
<item name="android:clickable">true</item>
</style>
<style name="HotseatButton.center">
<item name="android:paddingLeft">12dip</item>
<item name="android:paddingRight">12dip</item>
<item name="android:background">@drawable/hotseat_bg_center</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">fill_parent</item>
<item name="android:focusable">true</item>
<item name="android:clickable">true</item>
</style>
<style name="HotseatButton.Left">
<item name="android:layout_marginLeft">4dip</item>
<item name="android:background">@drawable/hotseat_bg_left</item>
</style>
<style name="HotseatButton.Right">
<item name="android:layout_marginRight">4dip</item>
<item name="android:background">@drawable/hotseat_bg_right</item>
</style>
3.5loadHotseats
PackageManager pm = getPackageManager();
for (int i=0; i<mHotseatConfig.length; i++) {
Intent intent = null;
if (mHotseatConfig[i].equals("*BROWSER*")) {
// magic value meaning "launch user's default web browser"
// replace it with a generic web request so we can see if there is indeed a default
String defaultUri = getString(R.string.default_browser_url);
intent = new Intent(
Intent.ACTION_VIEW,
((defaultUri != null)
? Uri.parse(defaultUri)
: getDefaultBrowserUri())
).addCategory(Intent.CATEGORY_BROWSABLE);
// note: if the user launches this without a default set, she
// will always be taken to the default URL above; this is
// unavoidable as we must specify a valid URL in order for the
// chooser to appear, and once the user selects something, that
// URL is unavoidably sent to the chosen app.
}
else if(mHotseatConfig[i].equals("*MESSAGE*"))
{
intent = new Intent(Intent.ACTION_VIEW);
intent.setType("vnd.android-dir/mms-sms");
}
else if(mHotseatConfig[i].equals("*CONTACT*"))
{
intent = new Intent();
intent.setAction(Intent.ACTION_PICK);
intent.setData(Contacts.People.CONTENT_URI);
}
else {
try {
intent = Intent.parseUri(mHotseatConfig[i], 0);
} catch (java.net.URISyntaxException ex) {
Log.w(TAG, "Invalid hotseat intent: " + mHotseatConfig[i]);
// bogus; leave intent=null
}
}
if (intent == null) {
mHotseats[i] = null;
mHotseatLabels[i] = getText(R.string.activity_not_found);
continue;
}
if (LOGD) {
Log.d(TAG, "loadHotseats: hotseat " + i
+ " initial intent=["
+ intent.toUri(Intent.URI_INTENT_SCHEME)
+ "]");
}
3.6setupViews
mHandleView = (HandleView) findViewById(R.id.all_apps_button);
mHandleView.setLauncher(this);
mHandleView.setOnClickListener(this);
mHandleView.setOnLongClickListener(this);
ImageView hotseatLeft0 = (ImageView) findViewById(R.id.hotseat_left0);
hotseatLeft0.setContentDescription(mHotseatLabels[0]);
hotseatLeft0.setImageDrawable(mHotseatIcons[0]);
ImageView hotseatLeft1 = (ImageView) findViewById(R.id.hotseat_left1);
hotseatLeft1.setContentDescription(mHotseatLabels[1]);
hotseatLeft1.setImageDrawable(mHotseatIcons[1]);
ImageView hotseatRight1 = (ImageView) findViewById(R.id.hotseat_right1);
hotseatRight1.setContentDescription(mHotseatLabels[2]);
hotseatRight1.setImageDrawable(mHotseatIcons[2]);
ImageView hotseatRight0 = (ImageView) findViewById(R.id.hotseat_right0);
hotseatRight0.setContentDescription(mHotseatLabels[3]);
hotseatRight0.setImageDrawable(mHotseatIcons[3]);
3.7launchHotSeat
//实际上就是点击了hotseat之后,转入对应的程序而已
@SuppressWarnings({"UnusedDeclaration"})
public void launchHotSeat(View v) {
if (isAllAppsVisible()) return;
int index = -1;
if (v.getId() == R.id.hotseat_left0) {
index = 0;
}
else if (v.getId() == R.id.hotseat_left1) {
index = 1;
}
else if (v.getId() == R.id.hotseat_right1) {
index = 2;
}
else if (v.getId() == R.id.hotseat_right0) {
index = 3;
}
// reload these every tap; you never know when they might change
loadHotseats();
if (index >= 0 && index < mHotseats.length && mHotseats[index] != null) {
Intent intent = mHotseats[index];
startActivitySafely(
mHotseats[index],
"hotseat"
);
}
}
4、 其他改进
有些用户需求是希望可以将按钮中的一个或者多个用户可以自己设置。这种情况下,就可以设置点击某个按钮,弹出一个选择框,可以让用户选择需要的那个
app。然后程序将这个app信息保存下来,比如使用reference来保存。然后在loadHotseats函数中读取reference中的信息进行配置。当然setupViews和launchHotSeat这两个函数也要对应修改就可以了。