Changing LayoutParams of always visible Chat Heads

2019-02-23 00:10发布

问题:

I am attempting to add a view I am generating via a service. The code I am using is based on Facebook Chatheads which are always visible, regardless of the apps state. They are also displayed above anything else too:

I now wish to constrain the chat head to the active app. Specifically I am dealing with a Bad Token Exception whenever I change the Window.LayoutParams from TYPE_PHONE to TYPE_DRAWN_APPLICATION.

MY QUESTION: I know that I am required to pass on the correct window token to the LayoutParams but can't seem to figure out how to do this correctly. Any advice would be highly appreciated.

Here is my code:

//Main Activity

private void addNewBubble() {
        BubbleLayout bubbleView = (BubbleLayout)LayoutInflater.from(MainActivity.this).inflate(R.layout.bubble_layout, null);
        bubblesManager.addBubble(bubbleView, 60, 20);
}

// initializes Bubbles Manager
private void initializeBubblesManager() {
        bubblesManager = new BubblesManager.Builder(this)
                .setTrashLayout(R.layout.task_bubble_trash_layout)
                .setInitializationCallback(new OnInitializedCallback() {
                    @Override
                    public void onInitialized() {
                        addNewBubble(); // Called when addNewBubble is initialized and the bubble data is loaded. When used on devices running API 18 or below, this function is always called.

                    }
                })
                .build();
        bubblesManager.initialize();
    }

// initializes Bubbles Manager
        private void initializeBubblesManager() {
            bubblesManager = new BubblesManager.Builder(this)
                    .setTrashLayout(R.layout.task_bubble_trash_layout)
                    .setInitializationCallback(new OnInitializedCallback() {
                        @Override
                        public void onInitialized() {
                            addNewBubble(); // Called when addNewBubble is initialized and the bubble data is loaded. When used on devices running API 18 or below, this function is always called.

                        }
                    })
                    .build();
            bubblesManager.initialize();
        }

//XML - Custom Bubble_layout

<com.momely.bubbles.BubbleLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:clipChildren="false"
    android:clipToPadding="false">

    <ImageView
        android:id="@+id/avatar"
        android:layout_width="70dp"
        android:layout_height="70dp"
        android:layout_gravity="center"
        android:background="@drawable/profile_decorator"
        android:src="@drawable/round_button"
        android:scaleType="centerCrop"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/white"
        android:textSize="15sp"
        android:layout_marginTop="2dp"
        android:layout_marginLeft="2dp"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:background="@drawable/bubble_counter_bkg"
        android:text="1"/>

</com.momely.bubbles.BubbleLayout>

//in bubblesManager

public class BubblesManager {
    private static BubblesManager INSTANCE;
    private Context context;
    private boolean bounded;
    private BubblesService bubblesService;
    private int trashLayoutResourceId;
    private OnInitializedCallback listener;


    //getInstance (called in Builder below)
    private static BubblesManager getInstance(Context context){
        if (INSTANCE == null) {
            INSTANCE = new BubblesManager(context);
        }
        return INSTANCE;
    }

    //Binds the service to the application
    private ServiceConnection bubbleServiceConnection = new ServiceConnection(){
        @Override
        public void onServiceConnected(ComponentName name, IBinder service){
            BubblesService.BubblesServiceBinder binder = (BubblesService.BubblesServiceBinder)service;
            BubblesManager.this.bubblesService = binder.getService();
            configureBubblesService();
            bounded = true;
            if(listener != null){
                listener.onInitialized();
            }
        }

   //Initializes Bubbles Manager
   private BubblesManager(Context context){
        this.context = context;
        }

   //Initializes the service
   public void initialize(){
        context.bindService(new Intent(context, BubblesService.class),
            bubbleServiceConnection,
            Context.BIND_AUTO_CREATE);
        }

    public void addBubble(BubbleLayout bubble, int x, int y){
        if(bounded){
            bubblesService.addBubble(bubble, x, y);
            Log.d("Bubble", "Bubble created");
        }

    //Builder class
    public static class Builder {
        private BubblesManager bubblesManager;

        //Builder constructor
        public Builder(Context context){
            this.bubblesManager = getInstance(context);
        }

        //Sets initialization Callbacks - a callback is when we provide a function as an argument to another function in order to enforce the order of operations.
        public Builder setInitializationCallback(OnInitializedCallback listener){
            bubblesManager.listener = listener;
            return this;
        }

        //Sets Trash Layout
        public Builder setTrashLayout(int trashLayoutResourceId){
            bubblesManager.trashLayoutResourceId = trashLayoutResourceId;
            return this;
        }

        //Triggers BubbleManager;
        public BubblesManager build(){
            return bubblesManager;
        }
    }
}

//in bubblesService

imports...


public class BubblesService extends Service{
    private BubblesServiceBinder binder = new BubblesServiceBinder();
    private List<BubbleLayout> bubbles = new ArrayList<>();
    private BubbleTrashLayout bubblesTrash;
    private WindowManager windowManager;
    private BubblesLayoutCoordinator layoutCoordinator;

    //overrides the IBind method
    @Override
    public IBinder onBind(Intent intent){
        return binder;
    }


    //overrides the onUnbind method
    @Override
    public boolean onUnbind(Intent intent){
        for (BubbleLayout bubble : bubbles){
            recycleBubble(bubble);
        }
        bubbles.clear();
        return super.onUnbind(intent);
   }


    //Gets the Windows Manager
    private WindowManager getWindowManager(){
        if (windowManager ==null){
            windowManager = (WindowManager)getSystemService(WINDOW_SERVICE);
        }
        return windowManager;
    }

    // Adds view to the Window
    public void addBubble(BubbleLayout bubble, int x, int y){
        WindowManager.LayoutParams layoutParams = buildLayoutParamsForBubble(bubble, x,y);
        layoutParams.token = bubble.getApplicationWindowToken();
        bubble.setWindowManager(getWindowManager());
        bubble.setViewParams(layoutParams);
        bubble.setLayoutCoordinator(layoutCoordinator);
        bubbles.add(bubble);
        addViewToWindow(bubble);
    }


    // Initializes the Layout Cocordinator
    private void initializeLayoutCoordinator(){
        layoutCoordinator = new BubblesLayoutCoordinator.Builder(this)
                .setWindowManager(getWindowManager())
                .setTrashView(bubblesTrash)
                .setTrashView(bubblesTrash)
                .build();
    }

    //Adds view to the Window
    private void addViewToWindow(final BubbleBaseLayout view){
        new Handler(Looper.getMainLooper()).post(new Runnable(){
            @Override
            public void run(){
                getWindowManager().addView(view, view.getViewParams());
            }
        });
    }

    //BUILDING LAYOUT PARAMS --> THIS IS WHERE THE TYPE IS SET
    private WindowManager.LayoutParams buildLayoutParamsForBubble(BubbleLayout bubble, int x, int y){
        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.TYPE_DRAWN_APPLICATION, //!!!! WHEN this is set to TYPE_PHONE the chat head stays on the screen even if the application is onPause.
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                PixelFormat.TRANSPARENT);
        params.gravity = Gravity.TOP | Gravity.START;
        params.token = bubble.getApplicationWindowToken();
        params.x = x;
        params.y = y;
        return params;
    }


    //defines the BubblesService Binder service
    public class BubblesServiceBinder extends Binder {
        public BubblesService getService(){
            return BubblesService.this;
        }
    }

}

///ERROR I AM RECEIVING

E/AndroidRuntime: FATAL EXCEPTION: main
   Process: com.momely.mascapone, PID: 16638
   android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
       at android.view.ViewRootImpl.setView(ViewRootImpl.java:683)
       at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
       at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93)
       at com.momely.bubbles.BubblesService$2.run(BubblesService.java:115)
       at android.os.Handler.handleCallback(Handler.java:751)
       at android.os.Handler.dispatchMessage(Handler.java:95)
       at android.os.Looper.loop(Looper.java:154)
       at android.app.ActivityThread.main(ActivityThread.java:6119)
       at java.lang.reflect.Method.invoke(Native Method)
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)

Any advice on how I could limit the chat head to the application window without it remaining on screen when the app is on Pause?

Z

回答1:

"I now wish to constrain the chat head to the active app."

I see two options. As a simple hack (keeping the Service) use Option 1.
Option 2 means copying BubblesService.java to BubblesLocal.java and BubblesManager.java to BubblesManagerLocal.java, and hacking out all the Service code. I suggest Option 1 is what you want (much easier, and you can turn it on and off).

Option 1

Simply hide the bubbles when your application is not active.
Add the following code to your project (tested, working):

MainActivity.java:

//update ActionBarActivity to AppCompatActivity 
`public class MainActivity extends AppCompatActivity //ActionBarActivity`
private boolean mStarted = false;
 @Override
protected void onCreate(Bundle savedInstanceState) {
...
        initializeBubblesManager();
        mStarted = true;
//------------------------------------------------------------------------------------------------
    @Override
    protected void onResume() 
    {
        Log.i("MainActivity:","onResume");
        super.onResume();
        if(mStarted) bubblesManager.showBubbles();
    }
//------------------------------------------------------------------------------------------------
    @Override
    protected void onPause() 
    {
        Log.i("MainActivity:","onPause");
        super.onPause();
        if(mStarted) bubblesManager.hideBubbles();
    }
//------------------------------------------------------------------------------------------------

BubblesManager.java:

//------------------------------------------------------------------------------------------------
    public void showBubbles()
    {
        if(bounded && bubbleServiceConnection != null)bubblesService.showBubbles();
    }//showBubbles
//------------------------------------------------------------------------------------------------
    public void hideBubbles()
    {
        if(bounded && bubbleServiceConnection != null)bubblesService.hideBubbles();
    }//hideBubbles
//------------------------------------------------------------------------------------------------

BubblesService.java:

//------------------------------------------------------------------------------------------------
    public void showBubbles()
    {
        if(bubbles.size() > 0)
        {
            for (BubbleLayout bubble : bubbles) 
            {
                bubble.showBubble();
            }
        }
    }//showBubbles
//------------------------------------------------------------------------------------------------
    public void hideBubbles()
    {
        if(bubbles.size() > 0)
        {
            for (BubbleLayout bubble : bubbles) 
            {
                bubble.hideBubble();
            }
        }
    }//hideBubbles
//------------------------------------------------------------------------------------------------

BubbleLayout.java:

//------------------------------------------------------------------------------------------------
    public void showBubble()
    {
            //View.GONE This view is invisible, and it doesn't take any space for layout purposes.
            //View.INVISIBLE This view is invisible, but it still takes up space for layout purposes.

        getRootView().setVisibility(View.VISIBLE);
    }//showBubble
//------------------------------------------------------------------------------------------------
    public void hideBubble()
    {
        getRootView().setVisibility(View.INVISIBLE);
    }//hideBubble
//------------------------------------------------------------------------------------------------