在android平台中,显示在HOME界面的一些挂件,即桌面小部件,被称为AppWidget。在自己的程序中适当地加入AppWidget,不但使用户更方便,也能从一定程序上提高本程序的留存率。
下面通过我所写的一个课表应用来说明如何使用AppWidget。
我所写的AppWidget最终结果如下图:
1.首先在res/layout下编写AppWidget的布局文件。
我的代码如下:
appwidget_small.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@drawable/appwidget_bg" >
<Button
android:id="@+id/widget_small_refresh"
android:layout_width="60dp"
android:layout_height="40dp"
android:layout_alignParentRight="true"
android:text="@string/widget_small_refrest"
android:textAppearance="@android:style/TextAppearance.Medium" />
<TextView
android:id="@+id/widget_small_day"
android:layout_width="60dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:layout_below="@id/widget_small_refresh"
android:gravity="center"
android:textAppearance="@android:style/TextAppearance.Small" />
<include
android:id="@+id/widget_small_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="2dp"
android:layout_marginLeft="2dp"
android:layout_marginTop="2dp"
android:layout_toLeftOf="@id/widget_small_refresh"
layout="@layout/main_list_item"
android:background="@drawable/appwidget_list_bg" />
</RelativeLayout>其中include的是课表信息部分的布局,它在我的MainActivity还用到,这里没有另外编写,直接使用include标签将它引用进来。
main_list_item.xml的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:id="@+id/list_item_class"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:singleLine="true"
android:textAppearance="@android:style/TextAppearance.Medium" />
<TextView
android:id="@+id/list_item_time"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:singleLine="true"
android:textAppearance="@android:style/TextAppearance.Small" />
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<LinearLayout
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:orientation="vertical" >
<TextView
android:id="@+id/list_item_course"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:singleLine="true"
android:textAppearance="@android:style/TextAppearance.Medium" />
<TextView
android:id="@+id/list_item_teacher"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:singleLine="true"
android:textAppearance="@android:style/TextAppearance.Medium" />
</LinearLayout>
<LinearLayout
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:orientation="vertical" >
<TextView
android:id="@+id/list_item_room"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:singleLine="true"
android:textAppearance="@android:style/TextAppearance.Medium" />
<TextView
android:id="@+id/list_item_week"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:singleLine="true"
android:textAppearance="@android:style/TextAppearance.Medium" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
在这里说明一下,对于appWidget的布局文件的根标签如果设置宽高为match_parent(fill_partent),则在HOME界面改变它的大小时它也会自动扩张,否则无论将它占的空间拉伸到多大,它都不会扩张。
2.在/res/xml下编写这个appwidget的信息文件。
widget_small_provider_info.xml代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/appwidget_small"
android:minHeight="60dp"
android:minWidth="240dp"
android:updatePeriodMillis="1800000" >
</appwidget-provider>上面initialLayout即初始的布局,minHeight和minWidth即最小的宽高,updatePeriodMillis为更新周期。
需要注意的是关于这个更新周期,在我本机G14,android4.0.3上发现它并没有用,百度之后发现它存在着BUG,在有些系统有效,有些则没效。所以如果想在任何机型都能自动更新的话,还要自己写一个service去更新。这个可参考android SDK中的例子。
3.接下来需要编写一个类,继承自AppWidgetProvider。
代码如下:
/*
* @(#)TableWidgetProvider.java Project:UniversityTimetable
* Date:2013-2-11
*
* Copyright (c) 2013 CFuture09, Institute of Software,
* Guangdong Ocean University, Zhanjiang, GuangDong, China.
* All rights reserved.
*
* 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.lurencun.cfuture09.universityTimetable.appwidget;
import java.util.Calendar;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.util.Log;
import android.widget.RemoteViews;
import com.lurencun.cfuture09.universityTimetable.R;
/**
* @Author Geek_Soledad (66704238@51uc.com)
* @Function
*/
public class TableSmallWidgetProvider extends AppWidgetProvider {
private static final String TAG = "TableSmallWidgetProvider";
public static final String ACTION_UPDATE = "cfuture09.universityTimetable.action.TIMETABLE.APPWIDGET_SMALL_UPDATE";
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
super.onDeleted(context, appWidgetIds);
Log.d(TAG, "onDeleted");
}
@Override
public void onDisabled(Context context) {
super.onDisabled(context);
Log.d(TAG, "onDisable");
}
@Override
public void onEnabled(Context context) {
super.onEnabled(context);
Log.d(TAG, "onEnabled");
}
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
Log.d(TAG, "onReceive");
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
Log.d(TAG, "update");
}
}
上面onEnabled在第一次创建AppWidget中执行。
在HOME界面中是可以多次插入同一个AppWidget的,每次插入一个AppWidget,onReceive和onUpdate都会被执行,这一次可以自己去做试验了解它的生命周期,在这里不赘述。
4.在Manifest中声明。
<receiver
android:name=".appwidget.TableSmallWidgetProvider"
android:label="@string/widget_small_4_1" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<intent-filter>
<action android:name="cfuture09.universityTimetable.action.TIMETABLE.APPWIDGET_SMALL_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_small_provider_info" />
</receiver>
其中label中引用的字符串即在插入Appwidget时出现的那个名字,如这里为"大学课程表(4*1)",如果不添加,默认为你的程序名,即在application标签中声明的label。然后加入的intent-filter,为其接收的广播,这里还定义了自己的一个action,它将在下面的例子中用到,因为我希望还能手动更新appwidget的数据。在上面的例子中,一个简单的AppWidget就完成了。
但是我需要的还不够,我还希望这个AppWidget的控件内容是可以改变的,而不是写死在布局文件中的,这就需要用到RemoteViews了。
因为在android中,AppWidget与你的主程序是运行在不同的进程当中的,在这里需要用RemoteViews来进行它们之间的通信,而不是像在Activity中那样方便。
而对于RemoteViews在不同版本的API中,支持的控件(亦其提供的方法)也不同,越往后支持的越多,在2.2中,貌似还不支持AbsListView控件的更新,也只提供了简单的onclick事件绑定的方法。
首先创建一个RemoteViwews对象,RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.appwidget_small);
然后可以通过它的setTextViewText方法设置AppWidget中的TextView的内容,传入的参数为要设置的textview的id和内容。
如果想点击它而打开你的程序的activity,或者更新控件,则也如下面代码所示。
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
Log.d(TAG, "onUpdate");
for (int i = 0; i < appWidgetIds.length; i++) {
RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
R.layout.appwidget_small);
// 设置星期几
remoteViews.setTextViewText(R.id.widget_small_day,
arrayWeeks[currentDay]);
// 打开程序
PendingIntent startIntent = PendingIntent.getActivity(context, 0,
new Intent(context, MainActivity.class), 0);
remoteViews.setOnClickPendingIntent(R.id.widget_small_content,
startIntent);
// 更新控件的事件绑定
Intent intent = new Intent();
intent.setAction(ACTION_UPDATE);
// 以发送广播消息的方式创建PendingIntent.
PendingIntent pending_intent = PendingIntent.getBroadcast(context,
0, intent, 0);
remoteViews.setOnClickPendingIntent(R.id.widget_small_refresh,
pending_intent);
appWidgetManager.updateAppWidget(appWidgetIds[i], remoteViews);
}
}
在更新控件中,由于它是发送了一个广播,onReceive将会被执行,但不会执行onUpdate方法,所以这里还需要再修改onReceive方法,否则无法达到更新的目的。
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "onReceive");
if (ACTION_UPDATE.equals(intent.getAction())) {
RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
R.layout.appwidget_small);
// 设置星期几
remoteViews.setTextViewText(R.id.widget_small_day,
arrayWeeks[currentDay]);
// 更新节课
updateWidgetViews(remoteViews, dto);
PendingIntent startIntent = PendingIntent.getActivity(context, 0,
new Intent(context, MainActivity.class), 0);
remoteViews.setOnClickPendingIntent(R.id.widget_small_content,
startIntent);
ComponentName componentName = new ComponentName(context,
TableSmallWidgetProvider.class);
AppWidgetManager.getInstance(context).updateAppWidget(
componentName, remoteViews);
} else {
super.onReceive(context, intent);
}
}
------- android培训、java培训、期待与您交流! ----------
上一篇博客主要是讲了些mp3播放的后台service的播放情况,当时还重点了歌词的显示。后来发现还欠缺了一块内容才算完成,我打算在这里补充完整。过程是这样的,当我们拿到一个lrc文件,这个lrc文件就是网上很流行的lrc格式的歌词文件。这样处理它呢?这样处理的合合适适让它在MP3播放的activity中显示相应时间点的歌词呢?
我们的原理是这样的,我们所以在编写的lrc文件的时候都必须遵照一些约定俗成的格式,这样方便我们这些编写音乐播放软件的人处理lrc文件。我们先来看一幅图,
粗略看一下这个普通的lrc文件之后,我们是这样处理的建立一个LinkedHashMap<Long, String>类型的对象后,将前面的时间[00:06.09]作为long型的key放到这个map中,其歌词内容“想约在一个适合聊天的下午”作为这个[00:06.09]的key的值放到map中。这样我们就可以方便的根据是时间显示歌词了。
我们从代码来完整的看一篇如果进行的。
首先我们要创建一个lrc的类,这个类有一些属性:歌名啊,作者啊,歌词内容等等,还要设置set和get方法,和复写 toString()的方法(为什么我再想想,反正我经常见到)
package com.music.mp3;
import java.util.LinkedHashMap;
public class Lrc {
private String title;
private String singer;
private String album;
private String lrcEditer;
private LinkedHashMap<Long,String> content;
public Lrc() {
super();
}
public Lrc(String title, String singer, String album,
String lrcEditer, LinkedHashMap<Long, String> content) {
super();
this.title = title;
this.singer = singer;
this.album = album;
this.lrcEditer = lrcEditer;
this.content = content;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getSinger() {
return singer;
}
public void setSinger(String singer) {
this.singer = singer;
}
public String getAlbum() {
return album;
}
public void setAlbum(String album) {
this.album = album;
}
public String getLrcEditer() {
return lrcEditer;
}
public void setLrcEditer(String lrcEditer) {
this.lrcEditer = lrcEditer;
}
public LinkedHashMap<Long, String> getContent() {
return content;
}
public void setContent(LinkedHashMap<Long, String> content) {
this.content = content;
}
@Override
public String toString() {
return "LrcInfo [album=" + album + ", lrcEditer=" + lrcEditer + ", singer=" + singer
+ ", title=" + title +", content=" + content+ "]";
}
}
其他处理过程如下:
1.拿到歌词lrc文件所在地点
String path=MainActivity.SDPath+"mp3/"+Mp3PlayerActivity.m.getMp3_name()+".lrc";
2、从这个文件读取资料到内存,打开一个输入流
InputStream inputStream=null;
inputStream = new FileInputStream(path);
3、建立一个对这个文件处理的对象,并且把输入流交给这个对象的方法,进行处理。
Lrc lrc_result=null;//这个是等下把处理结果交给返回给别人的。
LrcProcessor p=new LrcProcessor();
lrc_result=p.process(inputStream);
4、LrcProcessor 类型的process()方法进行处理的过程,处理的结果返回有一个lrc类型的对象l,具备lrc类型的想要的属性。
package com.music.lrc;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.LinkedHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.music.mp3.Lrc;
public class LrcProcessor {
//歌词对象
Lrc l=null;
//歌词对应毫秒值
Long lrcTime=null;
//对应显示的歌词
String content=null;
//用户保存所有的歌词和时间点信息间的映射关系的Map
private LinkedHashMap<Long, String> maps = new LinkedHashMap<Long, String>();
public Lrc process(InputStream in){
InputStreamReader re = null;//声明一个对输入流的reader对象
try {
re = new InputStreamReader(in,"GB2312");//reader对象读输入流,后一个参数是charsetName:identifies the character converter to use.我的理解就是不同编码
} catch (UnsupportedEncodingException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
BufferedReader reader=new BufferedReader(re);//声明缓冲池,这是常规处理
String temp=null;
l=new Lrc();//这个是等下要返回的对象,一个lrc类型的文件用于在播放的service里面调用
try {
while((temp=reader.readLine())!=null){//用temp一行一行的去读
setLrc(temp);//每读一次,就用一次这个setLrc文件的方法
}
l.setContent(maps);//将歌词的时间点(键)和歌词内容(值)这样一个maps交给l这个lrc类型的对象。(别的东西会对其处理,这里我们不要关心别的方法要对这个文件做什么)
} catch (IOException e) {
return null;
}
return l;//返回给调用这个函数的对象的信息
}
public void setLrc(String str){
if (str.startsWith("[ti:")) {//以这个开头就是曲目
String title = str.substring(1, str.length() - 1); //所有的歌曲曲目都是以[ti: 开头和]结尾,那么这样做是正确的。但是为什么从第一个开始读,而不是从第5个呢
//System.out.println("title--->" + title); //验证歌词读对了没有
l.setTitle(title);
}
// 取得歌手信息
else if (str.startsWith("[ar:")) {
String singer = str.substring(1, str.length() - 1);
//System.out.println("singer--->" + singer);
l.setSinger(singer);
}
// 取得专辑信息
else if (str.startsWith("[al:")) {
String album = str.substring(1, str.length() - 1);
// System.out.println("album--->" + album);
l.setAlbum(album);
}
else if (str.startsWith("[by:")) {
String lrcEditer = str.substring(1, str.length() - 1);
// System.out.println("album--->" + album);
l.setLrcEditer(lrcEditer);
//如果上面的都不是,那么肯定是歌曲内容了。
}else{
// 设置正则规则
String reg = "\\[(\\d{2}:\\d{2}\\.\\d{2})\\]";
// 编译 compile的意思就是编译,所以就是用reg这个规则来编译出一个模式
Pattern pattern = Pattern.compile(reg);
// 出了模式最后再用其与传入的str去匹配。注意,str是一行一行读出来的结果
Matcher matcher = pattern.matcher(str);
//返回的是Matcher对象,再通过该对象的find()方法就可以查询是否匹配了。
if(matcher.find()){
String msg=matcher.group();//如何记录对应的时间呢,Matcher的group(int i)返回匹配字符,并前后剔除i个字符。当然这里为0
String timeStr=msg.substring(1, msg.length()-1);//这是为了剔除时间前后的[]
lrcTime=parseTime(timeStr);//我们把时间转化为毫秒,因为在mp3播放的service里面我们都是以毫秒为单位。这样方便比较和操作
//下一句,我们用]作为分隔符,将一行内容"[03:07.77]我想哭 不敢哭"用]分隔后就会得到s[0]就是“[03:07.77” s[1]就是”我想哭 不敢哭“.
String[] s=str.split("\\]");//如果用“.”作为分隔的话,必须是如下写法:String.split("\\."),这样才能正确的分隔开,不能用String.split(".");“.”和“|”都是转义字符,必须得加"\\";
if(s.length>=2){
content=s[1];//用]来分割后,s[0]就是时间。而s[1]就是歌词内容
maps.put(lrcTime, content);//设置键值对
}
}
}
}
public Long parseTime(String timeStr){//将时间转换为毫秒的函数
String s[]=timeStr.split(":");
int min=Integer.parseInt(s[0]);
String ss[]=s[1].split("\\.");
int sec=Integer.parseInt(ss[0]);
int mil=Integer.parseInt(ss[1]);
//这样看来最后返回的毫秒数哦
return min*60*1000+sec*1000+mil*10L;//此处的L就相当于把int转换为Long
}
}
最后,我想再对其中的正则表达式再做一个说明。我们对[02:31.72]这样一个格式的排序写出了如下的正则表达式:
\\[(\\d{2}:\\d{2}\\.\\d{2})\\]
其中\\是针转义字符用的。或者说在正则表达式中,"."本来就有特殊的含义了,如果我要用匹配一个真正的“.”,就要使用“\\.”告诉这个正则表达式的算法,我们要得是真正的“.”。显然其中“:”在正则表达式中没有含义,所以我们直接使用“:”就可以了。
其中,d{2}表示,是一个2位数的数字
其中()框起来的部分表示,我们要调用String msg=matcher.group();来获取这()内的内容。在这里,我们是获取其中的时间之后再对其处理改为毫秒
好了,对此,我相信我对歌词信息处理和正则表达式啊之类的理解有加深了。
------- android培训、java培训、期待与您交流! ----------
继续接上篇 http://macken.iteye.com/blog/1816783
写写metagun的图片加载机制
libgdx的坐标系使用的是笛卡尔坐标系,原点位于左下角。由于计算机图形学的历史,图形的渲染基本都是左上角开始,这种渲染方式也比较舒服。因此需要设置一下坐标系的原点为左上角;设置代码
public final void init (Metagun metagun) {
this.metagun = metagun;
Matrix4 projection = new Matrix4();
projection.setToOrtho(0, 320, 240, 0, -1, 1);//设置坐标原点位于左上角 正常libgdx的坐标位于右下角
spriteBatch = new SpriteBatch(100);
spriteBatch.setProjectionMatrix(projection);
}
加载图片素材的代码如下
private static TextureRegion[][] split (String name, int width, int height, boolean flipX) {
Texture texture = new Texture(Gdx.files.internal(name));
int xSlices = texture.getWidth() / width;
int ySlices = texture.getHeight() / height;
TextureRegion[][] res = new TextureRegion[xSlices][ySlices];
for (int x = 0; x < xSlices; x++) {
for (int y = 0; y < ySlices; y++) {
res[x][y] = new TextureRegion(texture, x * width, y * height, width, height);
res[x][y].flip(flipX, true);// Y轴翻转
}
}
return res;
}
加载读取图片素材是以左上角为原点,由于之前设置将原点由左下角变更为左上角,对Y坐标进行了翻转,因此在绘图是也需要将图片素材的y坐标进行翻转,因此使用了flip(x,true)函数进行翻转。
人物在地图中可以向左或向右移动,每个方向的人物图片都一样,这里借用对图片进行X轴翻转flip(true,x)实现了只需要保存一个方向的图片即可。