android跨进程传递布局(RemoteViews跨进程的使用)

有一个进程需要在另一个进程显示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。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
我可以看到这段代码是 Java 语言编写的 Android 应用程序的通知栏更新方法。根据您的要求,我将对其进行修改,但是需要您提供更多上下文和明确的需求。 以下是一种可能的修改方式,该方式将此方法作为类的一部分,并将方法参数更改为更清晰的名称: ```java public class AkiNotificationManager { private static final String TAG = "AkiNotificationManager"; private static final int NOTIFICATION_ID = 1; private NotificationManager mAkiNotificationManager; private Notification.Builder mAkiBuilder; private RemoteViews mAkiRemoteViews; private AkiModel mAkiModel; private String mAkiCurTitle; private String mAkiCurArtist; private String mAkiCurAlbumPath; public AkiNotificationManager(Context context, AkiModel akiModel) { mAkiModel = akiModel; mAkiNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); mAkiBuilder = new Notification.Builder(context); mAkiRemoteViews = new RemoteViews(context.getPackageName(), R.layout.notification_layout); mAkiBuilder.setCustomContentView(mAkiRemoteViews); mAkiBuilder.setSmallIcon(R.drawable.ic_music_note); mAkiBuilder.setAutoCancel(false); } public void updateNotification(boolean isPlaying, boolean updateAlbum) { if (isPlaying) { mAkiRemoteViews.setImageViewBitmap(R.id.notification_iv_play, BitmapFactory.decodeResource(getResources(), R.drawable.iv_pause_grey)); } else { mAkiRemoteViews.setImageViewBitmap(R.id.notification_iv_play, BitmapFactory.decodeResource(getResources(), R.drawable.iv_play_grey)); } if (updateAlbum) { mAkiRemoteViews.setTextViewText(R.id.notification_top_song, mAkiCurTitle); mAkiRemoteViews.setTextViewText(R.id.notification_top_singer, mAkiCurArtist); mAkiModel.getAlbumBitmap(bitmapCallBack, mAkiCurAlbumPath); Log.d(TAG, "updateNotification: Updating Album Art: " + mAkiCurAlbumPath); } mAkiBuilder.setCustomContentView(mAkiRemoteViews); mAkiNotificationManager.notify(NOTIFICATION_ID, mAkiBuilder.build()); } private final BitmapCallBack bitmapCallBack = new BitmapCallBack() { @Override public void setBitmap(Bitmap bitmap) { mAkiRemoteViews.setImageViewBitmap(R.id.notification_iv_album, bitmap); mAkiBuilder.setCustomContentView(mAkiRemoteViews); mAkiNotificationManager.notify(NOTIFICATION_ID, mAkiBuilder.build()); } }; public void setCurSong(String title, String artist, String albumPath) { mAkiCurTitle = title; mAkiCurArtist = artist; mAkiCurAlbumPath = albumPath; } } ``` 以上代码包含了一个名为 `AkiNotificationManager` 的类,该类包含了一个在构造函数中初始化的 `NotificationManager`,并且该类包含了一个 `updateNotification` 方法,该方法接受两个参数:一个布尔值,用于指示当前是否正在播放,以及另一个布尔值,用于指示是否需要更新专辑封面。此外,该类还包含一个 `setCurSong` 方法,用于设置当前播放的歌曲信息。 请注意,此代码仅用于演示目的,并且可能需要进一步修改,以符合您的具体需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

言并肃

感谢大哥支持!您的鼓励是我动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值