每天一练 2015/04/06 widget的用法

本文详细介绍了如何在Android中创建并注册自定义小部件,包括布局文件、元数据文件的声明,以及在AndroidManifest文件中的注册步骤。此外,还提供了完整的例子,演示了如何实现小部件的交互与数据更新。

1.      声明一个widget的布局文件(.xml格式)置于res/layout文件夹下

2.      声明一个widget的元数据文件(.xml格式)置于res/xml文件夹下。在这个元数据文件中至少要声明如下几个属性:minHeight,minWidth,updatePeriodMillis和initialLayout。其中initalLayout的属性值就是第一步中建立的widget布局文件

3.      在AndroidManifest文件中“注册”widget。注册的时候使用的是<receiver>标签。为了体现与一般的receiver的区别,在<receiver>中必须有如下子节点:

<receiver android:name=”.自己的AppWidgetProvider实现类”> 

<intent-filter>

           <action android:name=”android.appwidget.action.APPWIDGET_UPDATE”>

</intent-filter>

<meta-dataandroid:name=”android.appwidget.provider”

               android:resource=”@xml/第2步中声明的widget元数据文件”

/>

</receiver>

4.      声明一个自己的AppWidgetProvider实现类。widget的新建,删除等动作都会触发相应的widget广播,这些广播需要通过继承AppWidgetProvider来接收。通过重写AppWidgetProvider的相应回调方法(onUpdate,onDelete,onEnable等),来处理事件发生时的行为。

注意:1. 因为AppWidgetProvider继承自BroadcastReceiver,那么在写相应的回调方法时必须要考虑ANR问题,不能超时。

            2. 所有widget的UI操作相关操作(例如UI的更新,数据的输入等等)都是不能直接在widget本身上面进行的,必须借助于PendingIntent、RemoteViews和AppWidgetManager来辅助完成

            3. 一个AppWidgetProvider可以对应多个widget。因为一个widget可以设定不同的大小,可以被反复添加到页面上,这样一个AppWidgetProvider就可能要在onUpdate等回调方法中去同时更新多个widget了

一个完整的例子:

本例提供一个widget,widget中有一个刷新按钮,每次点击时,都可以生成一个随机数字。点击widget的文本区域,可以打开一个activity,通过activity中的按钮也可以对widget中的内容进行刷新。

本例需要如下文件:

1)res/layout/simple_widget_layout.xml widget的布局文件

2) res/xml/simple_widet.xml widget的元数据文件

3)AndroidManifest.xml 注册相关activity,receiver,service

4)MyAppWidgetProvider.java 继承自AppWidgetProvider,监听widget的各种动作

5)RandomService.java 继承自Service,完成widget的刷新,以及与MainActivity的通信

6)MainActivity.java 继承自Activiy,提供一个与widget“绑定”组件,通过该Activity利用RandomService,也能进行widget的更新


1)res/layout/simple_widget_layout.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:padding="10dp"
    android:background="@drawable/widget_background"
     >

    <LinearLayout
        android:id="@+id/container"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_weight="1"
        android:orientation="vertical" >

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="Random Number"
            android:textAppearance="?android:attr/textAppearanceMedium" />

        <TextView
            android:id="@+id/tv_number"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:textStyle="bold"
            android:textAppearance="?android:attr/textAppearanceMedium" />
    </LinearLayout>

    <ImageButton 
        android:id="@+id/btn_refresh"
        android:layout_width="55dp"
        android:layout_height="55dp"
        android:layout_gravity="center_vertical"
        android:background="@null"
        android:contentDescription="@null"
        android:src="@android:drawable/ic_menu_rotate"
        />

</LinearLayout>

widget布局中有两个文本框,tv_title显示提示信息,tv_number显示随机数字,btn_refresh是一个刷新按钮,每次点击,都会生成一个随机数字显示在tv_number中。

2)res/xml/simple_widget.xml

<appwidget-provider
     xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="180dp"
    android:minHeight="40dp"
    android:updatePeriodMillis="86400000"
    android:initialLayout="@layout/simple_widget_layout"
    
   />
updatePeriodMillis的属性值官方建议不易短于30分钟。如果需求是频繁刷新widget,不要依赖于widget自身的这个时间刷新设定,而应该使用AlarmManager或者Intent等来完成刷新。
3)AndroidManifest.xml (局部)

<activity
            android:name="com.example.widgettest.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <receiver android:name=".MyAppWidgetProvider">
            
            <intent-filter >
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
            </intent-filter>
            <meta-data 
                android:name="android.appwidget.provider"
                android:resource="@xml/simple_widget"
                />
            
        </receiver>
        <service android:name="RandomService"></service>
一个activity,一个receiver(AppWidgetProvider是BroadcastReceiver的继承者),一个service

4)MyAppWidgetProvider

public class MyAppWidgetProvider extends AppWidgetProvider{
	@Override
	public void onUpdate(Context context, AppWidgetManager appWidgetManager,int[] appWidgetIds) {
		Intent intent=new Intent(context,RandomService.class);
		context.startService(intent);
		
	}
}
这里只监听onUpdate事件,该事件在widget被新建的时候会被回调。这里不做任何直接处理而是启动一个RandomService来进行数据的生成和widget的刷新

5)RandomService

public class RandomService extends Service{
	
	public static final String ACTION="com.example.widgettest.ACTION";
	public static int sRandomNumber;
	
	public static int getRandomNumber(){
		return sRandomNumber;
	}
	
	@Override
	public void onCreate() {
		super.onCreate();
		
	}
	
	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		
		sRandomNumber=(int) (Math.random()*100);
		
		RemoteViews rv=new RemoteViews(getPackageName(), R.layout.simple_widget_layout);
		rv.setTextViewText(R.id.tv_number, String.valueOf(sRandomNumber));
		
		PendingIntent pi1=PendingIntent.getService(this, 0, new Intent(this, RandomService.class), 0);
		rv.setOnClickPendingIntent(R.id.btn_refresh, pi1);
		
		PendingIntent pi2=PendingIntent.getActivity(this, 0, new Intent(this,MainActivity.class), 0);
		rv.setOnClickPendingIntent(R.id.container, pi2);
		
		AppWidgetManager manager=AppWidgetManager.getInstance(this);
		ComponentName widget=new ComponentName(this, MyAppWidgetProvider.class);
		manager.updateAppWidget(widget, rv);
		
		Intent broad=new Intent(ACTION);
		sendBroadcast(broad);
		
		return START_NOT_STICKY;
	}
	
	@Override
	public IBinder onBind(Intent intent) {
		// TODO Auto-generated method stub
		return null;
	}

}
这里onStartCommand一共做了如下几件事情:

i. 生成一个随机数

ii. 将widget包装成一个RemoteViews,利用RemoteViews提供的API设置组件应该显示的内容。这里的API设计非常讲究,签名是SetTextViewText而不是setTextView,意在明确RemoteViews是不能直接设置相关组件的内容的,只能提供相关相关组件应该显示的内容是什么

iii. 继续利用RemoteViews提供的API,使用PendingIntent为widget里面的相关组件绑定“监听器”

iv.  获得AppWidgetManager,利用AppWidgetManager来做widget的具体更新工作

v.  AppWidgetManager更新完毕后,Service发送一个广播(这个广播的主要作用是在MainActivity中更新widget时会用到的)

通过pi1,为widget的刷新按钮绑定了一个单击响应。每次在widget中单击刷新按钮,RandomService都会被启动,并生成一个随机数,然后通过RemoteViews、AppWidgetManager将结果刷新到widget。pi2为widget单击面板绑定了一个单击响应,当单击widget面板的时候会启动MainActivity。

非常重要的是,任何针对RemoteView的改动(比如显示内容的变化)或者设置(比如为按钮添加单击事件)必须调用AppWidgetManager的 updateAppwidget方法才会让这些改动或者设置生效。

6)MainActivity

public class MainActivity extends Activity {
	@ViewInject(R.id.tv_main_number)
	private TextView tv;
	@ViewInject(R.id.btn_main_generate)
	private Button btn;
	private BroadcastReceiver mReceiver=new BroadcastReceiver(){
		@Override
		public void onReceive(Context context, Intent intent) {
                    updateView();
		}
	};
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		ViewUtils.inject(this);
		updateView();
	}
	protected void updateView() {
		tv.setText(String.valueOf(RandomService.getRandomNumber()));
	}
	@OnClick({R.id.btn_main_generate})
	public void doClick(View view){
		startService(new Intent(this,RandomService.class));
	}
	
	@Override
	protected void onResume() {
		super.onResume();
		IntentFilter filter=new IntentFilter(RandomService.ACTION);
		registerReceiver(mReceiver, filter);
	}
	@Override
	protected void onPause() {
		unregisterReceiver(mReceiver);
		super.onPause();
	}

}
R.layout.activity_main的内容如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <Button
        android:id="@+id/btn_main_generate"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Generate Number" >
    </Button>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Current Random Number"
        android:textAppearance="?android:attr/textAppearanceLarge" />

    <TextView
        android:id="@+id/tv_main_number"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textSize="55sp"
        android:textStyle="bold" />

</LinearLayout>
当MainActivity新建的时候,tv_main_number会从RandomService中拿数据显示出当前widget显示的数字。当点击btn_main_generate的时候,会启动RandomService,此时如果MainActivity保持在前台,那么它会收到RandomService更新完widget界面后发送的广播。在onReceive方法中,会去更新MainActivity的tv_main_number的内容,该数字显然与widget中的数字是一样的,都是RandomService的sRandomNumber。





 

我正在开发一个适老化服务平台。 from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QFrame, QPushButton, QDialog, QHBoxLayout, QMessageBox, QVBoxLayout, QRadioButton, QButtonGroup from PySide6.QtCore import Qt, QUrl from PySide6.QtMultimedia import QMediaPlayer from PySide6.QtMultimediaWidgets import QVideoWidget from PySide6.QtGui import QPixmap, QPalette, QBrush, QLinearGradient, QColor import os class VideoPlayerDialog(QDialog): def __init__(self, video_path, parent=None): super().__init__(parent) self.setWindowTitle("训练视频播") self.setMinimumSize(640, 480) layout = QVBoxLayout(self) self.video_widget = QVideoWidget() layout.addWidget(self.video_widget) self.player = QMediaPlayer(self) self.player.setVideoOutput(self.video_widget) self.player.setSource(QUrl.fromLocalFile(video_path)) self.player.play() self.player.errorOccurred.connect(self.handle_error) def handle_error(self, error, error_string): print(f"播错误: {error}, {error_string}") QMessageBox.warning(self, "播错误", f"无法播视频: {error_string}") class VideoSelectionDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("选择训练视频") self.setMinimumSize(300, 200) layout = QVBoxLayout(self) # Video options self.button_group = QButtonGroup() video_options = [ ("颈椎操", os.path.abspath(r"Data/Neck.mp4")), ("下肢力量训练", os.path.abspath(r"Data/Leg.mp4")), ("平衡训练", os.path.abspath(r"Data/Balance.mp4")), ] # Add radio buttons first_button = None for name, path in video_options: radio = QRadioButton(name) radio.setStyleSheet("font-size: 16px; color: #2c3e50;") # Simplified stylesheet self.button_group.addButton(radio) layout.addWidget(radio) if first_button is None: first_button = radio # Store the first button for default selection if not os.path.exists(path): radio.setToolTip(f"视频文件未找到: {path}") # Only set tooltip, keep enabled # Set the first button as checked by default if first_button: first_button.setChecked(True) # Play button play_btn = QPushButton("播") play_btn.setStyleSheet(""" QPushButton { font-size: 18px; background: #2580d6; color: #fff; border-radius: 8px; padding: 8px 20px; } QPushButton:hover { background: #1b5ea4; } """) play_btn.clicked.connect(self.play_selected_video) layout.addWidget(play_btn, alignment=Qt.AlignCenter) layout.addStretch() def play_selected_video(self): selected_button = self.button_group.checkedButton() if selected_button: video_name = selected_button.text() video_map = { "颈椎操": os.path.abspath(r"Data/Neck.mp4"), "下肢力量训练": os.path.abspath(r"Data/Leg.mp4"), "平衡训练": os.path.abspath(r"Data/Balance.mp4"), } video_path = video_map.get(video_name) if os.path.exists(video_path): dialog = VideoPlayerDialog(video_path, self) dialog.exec() else: QMessageBox.warning(self, "提示", f"未找到本地视频文件:{video_path}") else: QMessageBox.warning(self, "提示", "请选择一个训练视频!") self.accept() class RehabTrainingPage(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): # Main layout layout = QVBoxLayout(self) layout.setContentsMargins(20, 15, 20, 15) layout.setSpacing(10) layout.setAlignment(Qt.AlignCenter) # Set gradient background (light green to white) with background image self.setAutoFillBackground(True) palette = QPalette() gradient = QLinearGradient(0, 0, 0, self.height()) gradient.setColorAt(0, QColor("#E0F7E9")) # Light green at the top gradient.setColorAt(1, QColor("#FFFFFF")) # White at the bottom palette.setBrush(QPalette.Window, QBrush(gradient)) self.setPalette(palette) # Add a semi-transparent background image bg_label = QLabel(self) bg_label.setFixedSize(self.size()) pixmap = QPixmap("../Data/RehabTraining") if not pixmap.isNull(): bg_label.setPixmap(pixmap.scaled(500, 300, Qt.KeepAspect, Qt.SmoothTransformation)) bg_label.setStyleSheet("opacity: 0.3;") else: print("无法加载背景图片") bg_label.lower() # Title title = QLabel("康复训练") title.setStyleSheet("font-size: 28px; font-weight: bold; color: #2580d6;") layout.addWidget(title, alignment=Qt.AlignCenter) # Description desc = QLabel("为您推荐适合老年人的康复训练项目,帮助您增强体质、预防疾病。") desc.setStyleSheet("font-size: 18px; color: #2c3e50;") desc.setWordWrap(True) desc.setAlignment(Qt.AlignCenter) layout.addWidget(desc) # Training programs frame train_frame = QFrame() train_frame.setStyleSheet("background: #f5faff; border-radius: 10px; font-size: 16px; padding: 10px;") train_frame.setFixedWidth(400) train_layout = QVBoxLayout(train_frame) train_layout.setSpacing(5) train_layout.addWidget(QLabel("1. 颈椎操(每日2次,每次10分钟)")) train_layout.addWidget(QLabel("2. 下肢力量训练(每日1次,每次15分钟)")) train_layout.addWidget(QLabel("3. 平衡训练(每日1次,每次10分钟)")) layout.addWidget(train_frame, alignment=Qt.AlignCenter) # Watch video button btn = QPushButton("选择并观看视频") btn.setStyleSheet(""" QPushButton { font-size: 18px; background: #2580d6; color: #fff; border-radius: 8px; padding: 8px 20px; } QPushButton:hover { background: #1b5ea4; } """) btn.clicked.connect(self.open_video_selection) layout.addWidget(btn, alignment=Qt.AlignCenter) layout.addStretch() # Handle resize events to adjust background image and gradient def resize_background(event): bg_label.setFixedSize(self.size()) if not pixmap.isNull(): bg_label.setPixmap(pixmap.scaled(self.size(), Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)) new_gradient = QLinearGradient(0, 0, 0, self.height()) new_gradient.setColorAt(0, QColor("#E0F7E9")) new_gradient.setColorAt(1, QColor("#FFFFFF")) palette.setBrush(QPalette.Window, QBrush(new_gradient)) self.setPalette(palette) self.resizeEvent = lambda event: (resize_background(event), QWidget.resizeEvent(self, event)) def open_video_selection(self): dialog = VideoSelectionDialog(self) dialog.exec() 请检查为什么无法加载背景图片
05-12
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值