有一个进程需要在另一个进程显示UI时,开始考虑用反射的方法来加载布局xml,但考虑到要响应点击事件,排除掉此做法。通过多方查找,发现类似桌面Widget和通知栏的跨进程布局加载都潜移默化的使用RemoteViews,发现RemoteViews实现Parcelable,可支持跨进程传输(RemoteViews区别于View),相当于把View布局传入RemoteViews再传递给另一个进程加载。下面为实现跨进程加载的步骤:
先从服务端开始,服务端进程remoteviewstest主要是运行着MyService服务,客户端绑定此服务,此服务通过WindowManager添加窗口来加载客户端RemoteViews的布局。完整代码如下:
package com.example.remoteviewstest;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.RemoteViews;
import android.widget.TextView;
public class MyService extends Service {
private WindowManager mWindowManager;
private RemoteViews mRemoteViews;
private WindowManager.LayoutParams mLayoutParams;
private View mView;
private final Handler mUIHandler = new Handler();
private boolean mIsWindowAdd;
private final ICustomViewLoader mBinder = new ICustomViewLoader.Stub() {
@Override
public void addCustomView(RemoteViews remoteViews) throws RemoteException {
mRemoteViews = remoteViews;
mUIHandler.post(new Runnable() {
@Override
public void run() {
if (!mIsWindowAdd) {
mIsWindowAdd = true;
mView = mRemoteViews.apply(MyService.this, null);
mLayoutContent.removeAllViews();
mLayoutContent.addView(mView);
mWindowManager.addView(mLayoutWindow, mLayoutParams);
}
}
});
}
@Override
public void updateCustomView(RemoteViews remoteViews) throws RemoteException {
if (mView != null) {
remoteViews.reapply(MyService.this, mView);
}
}
@Override
public void removeCustomView() throws RemoteException {
mUIHandler.post(new Runnable() {
@Override
public void run() {
if (mIsWindowAdd) {
mWindowManager.removeView(mLayoutWindow);
mRemoteViews = null;
mIsWindowAdd = false;
}
}
});
}
};
private View mLayoutWindow;
private FrameLayout mLayoutContent;
@Override
public void onCreate() {
super.onCreate();
mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
LayoutInflater layoutInflater = LayoutInflater.from(this);
mLayoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
mLayoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
mLayoutParams.height = 400;
mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
mLayoutParams.gravity = Gravity.BOTTOM;
mLayoutWindow = layoutInflater.inflate(R.layout.layout_window, null, false);
mLayoutContent = mLayoutWindow.findViewById(R.id.layout_content);
}
@Override
public IBinder onBind(Intent intent) {
return mBinder.asBinder();
}
}
其中mLayoutWindow为窗口,mLayoutContent添加RemoteViews的布局,通过RemoteViews的apply来加载跨进程布局,reappley来刷新跨进程布局。通过创建AIDL接口ICustomViewLoader,供客户端调用,提供添加、删除和更新。
// ICustomViewLoader.aidl
package com.example.remoteviewstest;
import android.widget.RemoteViews;
interface ICustomViewLoader {
void addCustomView(in RemoteViews remoteViews);
void updateCustomView(in RemoteViews remoteViews);
void removeCustomView();
}
客户端主要通过CustomViewManager 类实现与服务端交互,具体代码如下:
package com.example.surfaceviewtest;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.widget.RemoteViews;
import com.example.remoteviewstest.ICustomViewLoader;
public class CustomViewManager {
public static final String TAG = CustomViewManager.class.getSimpleName();
public static final String ACTION_CUSTOM_TAG = "action_custom_click_";
private ICustomViewLoader mCustomViewLoader;
private final Context mContext;
private RemoteViews mRemoteViews;
public CustomViewManager(Context context) {
mContext = context.getApplicationContext();
}
private final BroadcastReceiver mCustomClickReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action != null && action.contains(ACTION_CUSTOM_TAG)) {
if (mCustomClickListener != null) {
mCustomClickListener.onRemoteClick(Integer.parseInt(action.replace(ACTION_CUSTOM_TAG, "")));
}
}
}
};
public void bindService() {
if (mContext == null) {
return;
}
if (mCustomViewLoader == null) {
Intent intent = new Intent("com.example.remoteviewstest.action");
intent.setPackage("com.example.remoteviewstest");
mContext.bindService(intent, mCarVideoServiceConnection, Context.BIND_AUTO_CREATE);
}
}
public void unbindService() {
if (mContext != null) {
if (mCustomViewLoader != null) {
mContext.unbindService(mCarVideoServiceConnection);
}
}
}
private final ServiceConnection mCarVideoServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mCustomViewLoader = ICustomViewLoader.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mCustomViewLoader = null;
}
};
/**
* 通过RemoteViews向服务端添加自定义布局
*
* @param layoutId 要在远程进程中显示的layoutId
* @param customIds 需要设置点击事件的id数组
* @param listener 点击事件监听
*/
public void registerCustomView(int layoutId, int[] customIds, final CustomClickListener listener) {
if (mContext == null) {
return;
}
if (listener != null) {
mCustomClickListener = listener;
}
mRemoteViews = new RemoteViews(mContext.getPackageName(), layoutId);
if (customIds != null && customIds.length > 0) {
IntentFilter intentFilter = new IntentFilter();
for (int id : customIds) {
Log.d(TAG, "registerCustomView:" + id);
Intent intent = new Intent(ACTION_CUSTOM_TAG + id);
intent.setPackage(mContext.getPackageName());
intentFilter.addAction(ACTION_CUSTOM_TAG + id);
PendingIntent pIntent = PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
mRemoteViews.setOnClickPendingIntent(id, pIntent);
}
mContext.registerReceiver(mCustomClickReceiver, intentFilter);
}
addCustomView(mRemoteViews);
}
/**
* 设置view是否使能,做对应选中状态背景色和图片的改变,由于setSelected在RemoteViews中不支持,
* 故使用采用setBackgroundResource来替代选中时background的状态切换
*/
public void setViewsSelected(int id, boolean selected) {
if (mRemoteViews != null) {
mRemoteViews.setInt(id, "setBackgroundResource", selected ? R.color.color_selected : R.color.color_normal);
updateCustomView(mRemoteViews);
}
}
public void unregisterCustomView() {
if (mContext != null) {
try {
mContext.unregisterReceiver(mCustomClickReceiver);
mCustomViewLoader.removeCustomView();
mRemoteViews = null;
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
public void addCustomView(RemoteViews remoteViews) {
try {
if (mCustomViewLoader != null) {
mCustomViewLoader.addCustomView(remoteViews);
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
public void updateCustomView(RemoteViews remoteViews) {
try {
if (mCustomViewLoader != null) {
mCustomViewLoader.updateCustomView(remoteViews);
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
private CustomClickListener mCustomClickListener;
public interface CustomClickListener {
void onRemoteClick(int id);
}
}
通过 mRemoteViews.setOnClickPendingIntent(id, pIntent);来实现点击事件的监听,这里的PendingIntent通过为PendingIntent.getBroadcast(),由于没有明确意图去启动某个界面,点击的具体操作由客户端自己实现,如果由明确意图可通过PendingIntent.getActivity()实例化PendingIntent来启动特定的Activity。setViewsSelected方法为RemoteViews通过反射调用setBackgroundResource方法来动态设置背景。
使用方式如下:
package com.example.surfaceviewtest;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity implements CustomViewManager.CustomClickListener {
private CustomViewManager mCustomViewManager;
private boolean mEnable = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
startService(new Intent(this, MyService.class));
Button btnAdd = findViewById(R.id.btn_add_view);
Button btnRemove = findViewById(R.id.btn_remove_view);
final int[] customIds = new int[]{R.id.tv_button1};
btnAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mCustomViewManager.registerCustomView(R.layout.layout_remoteviews, customIds, MainActivity.this);
}
});
btnRemove.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mCustomViewManager.unregisterCustomView();
}
});
mCustomViewManager = new CustomViewManager(this);
mCustomViewManager.bindService();
}
@Override
protected void onDestroy() {
mCustomViewManager.unbindService();
super.onDestroy();
}
@Override
public void onRemoteClick(int id) {
if (id == R.id.tv_button1) {
mCustomViewManager.setViewsSelected(R.id.tv_button1, !mEnable);
mEnable = !mEnable;
}
}
}
onRemoteClick为服务端回调的点击事件,setViewsSelected设置选中/非选中状态。
layout_remoteviews.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tv_button1"
android:tag="button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/color_normal"
android:text="测试按钮"
android:textColor="@android:color/white"
android:textSize="20sp"/>
</LinearLayout>
color资源文件在RemoteViews中同样支持按下press状态的处理。
<?xml version="1.0" encoding="utf-8"?>
<!--color_normal.xml-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@android:color/darker_gray" android:state_pressed="true"/>
<item android:color="@android:color/holo_green_dark" />
</selector>
<?xml version="1.0" encoding="utf-8"?>
<!--color_selected.xml-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@android:color/darker_gray" android:state_pressed="true"/>
<item android:color="@android:color/holo_red_dark" />
</selector>
最终效果如下:
android源码中@RemotableViewMethod注解的方法才支持,只有setEnabled为RemoteViews支持的方法,而setSelected和setActivated方法不支持,要实现选中只能从setEnabled方法或者setBackgroundResource方法入手。
@RemotableViewMethod
public void setEnabled(boolean enabled) {
if (enabled != this.isEnabled()) {
this.setFlags(enabled ? 0 : 32, 32);
this.refreshDrawableState();
this.invalidate(true);
if (!enabled) {
this.cancelPendingInputEvents();
}
}
}
@RemotableViewMethod
public void setBackgroundResource(int resid) {
if (resid == 0 || resid != this.mBackgroundResource) {
Drawable d = null;
if (resid != 0) {
d = this.mContext.getDrawable(resid);
}
this.setBackground(d);
this.mBackgroundResource = resid;
}
}
RemoteViews支持的View类型如下:
Layout:FrameLyout、LinearLayout、RelativeLayout、GridLayout
View:Button、ImageView、ImageButton、ProgressBar、TextView、ListView、GridView、StackView、ViewStub、AdapterViewFlipper、ViewFlipper、AnalogClock、Chronometer。