安卓展示浮动小部件叠加不起作用

7

我希望我的应用程序能像Facebook Messenger一样显示浮动气泡通知。根据https://www.androidhive.info/2016/11/android-floating-widget-like-facebook-chat-head/中的示例,以下是我编写的服务。

import android.annotation.SuppressLint;
import android.app.Service;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.IBinder;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;

public class BubbleNotifyService extends Service {

    private WindowManager windowManager;
    private View BubbleView;
    private TextView bubbleTitle, bubbleData;

    public BubbleNotifyService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @SuppressLint({"RtlHardcoded", "InflateParams"})
    @Override
    public void onCreate() {
        super.onCreate();
        //android.os.Debug.waitForDebugger();

        setTheme(R.style.AppTheme);

        BubbleView =
                LayoutInflater.from(this).inflate(R.layout.floating_bubble, null);

        final WindowManager.LayoutParams params;

    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
        params = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                PixelFormat.TRANSLUCENT
        );
    } else {
        params = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.TYPE_PHONE,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE ,
                PixelFormat.TRANSLUCENT
        );
    }

        if (BubbleView == null) {
            Log.d("Bubble", "BubbleView is null.");
        }

        params.gravity = Gravity.TOP | Gravity.LEFT;
        params.x = 0;
        params.y = 100;

        windowManager =
                (WindowManager) getSystemService(WINDOW_SERVICE);

        if (windowManager != null) {
            Log.d("Bubble", "added bubble to view.");
            windowManager.addView(BubbleView, params);
        } else {
            Log.d("Bubble", "windowManager is null");
        }

        final View collapsedView =
                BubbleView.findViewById(R.id.collapsed_view);
        final View expandedView =
                BubbleView.findViewById(R.id.expanded_view);

        expandedView.setVisibility(View.GONE);
        collapsedView.setVisibility(View.VISIBLE);


        ImageView close_collapsed = BubbleView.findViewById(R.id.collaspsed_cancel);
        ImageView close_expanded = BubbleView.findViewById(R.id.expanded_cancel);
        Button open_act_btn = BubbleView.findViewById(R.id.open_full_btn);
        bubbleTitle = BubbleView.findViewById(R.id.bubble_title);
        bubbleMeaning = BubbleView.findViewById(R.id.bubble_data);

        close_collapsed.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                stopSelf();
            }
        });

        close_expanded.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                collapsedView.setVisibility(View.VISIBLE);
                expandedView.setVisibility(View.GONE);
            }
        });

        open_act_btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                //TODO new Activity
            }
        });

        BubbleView.findViewById(R.id.bubble_root).setOnTouchListener(new View.OnTouchListener() {

            private int initialX;
            private int initialY;
            private float initialTouchX;
            private float initialTouchY;

            @SuppressLint("ClickableViewAccessibility")
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {

                switch (motionEvent.getAction()) {

                    case MotionEvent.ACTION_DOWN:
                        initialX = params.x;
                        initialY = params.y;

                        initialTouchX = motionEvent.getRawX();
                        initialTouchY = motionEvent.getRawY();
                        return true;

                    case MotionEvent.ACTION_UP:
                        int Xdiff = (int) (motionEvent.getRawX() - initialTouchX);
                        int Ydiff = (int) (motionEvent.getRawY() - initialTouchY);

                        if (Xdiff < 10 && Ydiff < 10) {
                            if (isViewCollapsed()) {
                                collapsedView.setVisibility(View.GONE);
                                expandedView.setVisibility(View.VISIBLE);
                            }
                        }
                        return true;

                    case MotionEvent.ACTION_MOVE:
                        params.x =
                                initialX + (int) (motionEvent.getRawX() - initialTouchX);
                        params.y =
                                initialY + (int) (motionEvent.getRawY() - initialTouchY);

                        windowManager.updateViewLayout(BubbleView, params);
                        return true;
                }
                return false;
            }
        });

    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        super.onStartCommand(intent, flags, startId);

        String title = intent.getStringExtra(IntentKeys.BUBBLE_DATA_TITLE);
        String data = intent.getStringExtra(IntentKeys.BUBBLE_DATA);

        bubbleTitle.setText(title);
        bubbleData.setText(data);

        return START_NOT_STICKY;
    }

    private boolean isViewCollapsed() {
        return BubbleView == null ||
                BubbleView.findViewById(R.id.collapsed_view).getVisibility() == View.VISIBLE;
    }


    @Override
    public void onDestroy() {
        super.onDestroy();

        if (BubbleView != null) {
            windowManager.removeView(BubbleView);
        }
    }
}

BubbleNotifyService 是从另一个接收通知的服务调用的:

Intent intent = new Intent(context, BubbleNotifyService.class);
                intent.putExtra(IntentKeys.BUBBLE_DATA_TITLE, Results.getWord());
                intent.putExtra(IntentKeys.BUBBLE_DATA, Results.getData());
                context.startService(intent);

所有来自意图的数据都传递给了 BubbleNotifyService。该服务作为一个独立的进程运行,如清单文件中所指定的 android:process=":BubbleResult",但该服务不会显示任何悬浮窗口。该应用程序已被授予在其他应用程序上绘制的权限。
气泡布局(floating_bubble.xml):
<?xml version="1.0" encoding="utf-8"?>

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/bubble_frame"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <android.support.constraint.ConstraintLayout
        android:id="@+id/bubble_root"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">


        <android.support.constraint.ConstraintLayout
            android:id="@+id/collapsed_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="visible">

            <ImageView
                android:id="@+id/collapsed_icon"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:srcCompat="@mipmap/ic_launcher"
                tools:ignore="ContentDescription" />

            <ImageView
                android:id="@+id/collaspsed_cancel"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintStart_toEndOf="@+id/collapsed_icon"
                app:layout_constraintTop_toTopOf="parent"
                app:srcCompat="@drawable/ic_action_cancel"
                tools:ignore="ContentDescription" />

        </android.support.constraint.ConstraintLayout>

        <android.support.v7.widget.CardView
            android:id="@+id/expanded_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:visibility="visible"
            app:cardCornerRadius="5dp"
            app:cardElevation="10dp"
            app:cardUseCompatPadding="true"
            app:contentPadding="5dp">

            <android.support.constraint.ConstraintLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <TextView
                    android:id="@+id/bubble_title"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="8dp"
                    android:textSize="24sp"
                    android:textStyle="bold"
                    app:layout_constraintEnd_toStartOf="@id/expanded_cancel"
                    app:layout_constraintHorizontal_bias="0.0"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toTopOf="parent"
                    tools:text="@string/result_card_title" />

                <TextView
                    android:id="@+id/bubble_data"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:textSize="18sp"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toBottomOf="@id/bubble_title"
                    tools:text="@string/result_card_data" />

                <ImageView
                    android:id="@+id/expanded_cancel"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintStart_toEndOf="@id/bubble_title"
                    app:srcCompat="@drawable/ic_action_cancel"
                    tools:ignore="ContentDescription" />

                <Button
                    android:id="@+id/open_full_btn"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginEnd="8dp"
                    android:layout_marginTop="8dp"
                    android:text="@string/result_card_see_more"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toBottomOf="@+id/bubble_data" />


            </android.support.constraint.ConstraintLayout>
        </android.support.v7.widget.CardView>

    </android.support.constraint.ConstraintLayout>


</FrameLayout>

  1. 你正在测试哪个设备?
  2. 这段代码会崩溃吗?(请发布错误日志)。
- Khemraj Sharma
@Khemraj 我已在实体 Moto G5 和 Pixel 2 模拟器上进行了测试。应用程序没有崩溃。服务成功运行,并创建了一个进程,但是覆盖层未显示。 - Pushkar
我为您发布我的工作代码。 - Khemraj Sharma
找出错误是在Java还是布局方面。可以尝试这种方式而不是使用布局:BubbleView = new TextView(this); BubbleView.setText("Some Text for Test") 如果问题解决了,那么您的布局可能存在问题或没有内容显示,例如TextView中的文本或ImageView中的图像或布局宽度为0。 - ygngy
3个回答

6

我测试了您的代码和提到的博客文章中的代码。问题似乎是因为您在布局中使用了ConstraintLayoutCardView

对于小部件行为,这些视图类型不受支持。当我们直接将视图添加到系统窗口时,它们的行为类似于RemoteViews。一旦您更改ConstraintLayoutCardView,您的布局就变得可见和可操作。


如果我想在浮动小部件中使用类似卡片的视图,有什么替代方案吗? - Pushkar
你可以玩转阴影。使用简单的RelativeLayout,将背景设置为阴影可绘制对象。这将会产生类似卡片视图的效果。阴影和适当的背景颜色是关键。 - theJango

3

以下是我的工作代码。您需要在需要显示浮动视图时启动此服务。

FloatingService.class

import android.app.Service;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.Build;
import android.os.IBinder;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.Toast;

public class FloatingService extends Service {

    private WindowManager mWindowManager;
    private View mFloatingView;

    public FloatingService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        //Inflate the floating view layout we created
        mFloatingView = LayoutInflater.from(this).inflate(R.layout.layout_floating_widget, null);
        int LAYOUT_FLAG;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            LAYOUT_FLAG = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        } else {
            LAYOUT_FLAG = WindowManager.LayoutParams.TYPE_PHONE;
        }
        //Add the view to the window.
        final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                LAYOUT_FLAG,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                PixelFormat.TRANSLUCENT);

        //Specify the view position
        params.gravity = Gravity.TOP | Gravity.LEFT;        //Initially view will be added to top-left corner
        params.x = 0;
        params.y = 100;

        //Add the view to the window
        mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
        mWindowManager.addView(mFloatingView, params);

        //The root element of the collapsed view layout
        final View collapsedView = mFloatingView.findViewById(R.id.collapse_view);
        //The root element of the expanded view layout
        final View expandedView = mFloatingView.findViewById(R.id.expanded_container);


        //Set the close button
        ImageView closeButtonCollapsed = (ImageView) mFloatingView.findViewById(R.id.close_btn);
        closeButtonCollapsed.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //close the service and remove the from from the window
                stopSelf();
            }
        });

        //Set the view while floating view is expanded.
        //Set the play button.
        ImageView playButton = (ImageView) mFloatingView.findViewById(R.id.play_btn);
        playButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(FloatingService.this, "Playing the song.", Toast.LENGTH_LONG).show();
            }
        });

        //Set the next button.
        ImageView nextButton = (ImageView) mFloatingView.findViewById(R.id.next_btn);
        nextButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(FloatingService.this, "Playing next song.", Toast.LENGTH_LONG).show();
            }
        });

        //Set the pause button.
        ImageView prevButton = (ImageView) mFloatingView.findViewById(R.id.prev_btn);
        prevButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(FloatingService.this, "Playing previous song.", Toast.LENGTH_LONG).show();
            }
        });

        //Set the close button
        ImageView closeButton = (ImageView) mFloatingView.findViewById(R.id.close_button);
        closeButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                collapsedView.setVisibility(View.VISIBLE);
                expandedView.setVisibility(View.GONE);
            }
        });

        //Open the application on thi button click
        ImageView openButton = (ImageView) mFloatingView.findViewById(R.id.open_button);
        openButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //Open the application  click.
                Intent intent = new Intent(FloatingService.this, MainActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                startActivity(intent);

                //close the service and remove view from the view hierarchy
                stopSelf();
            }
        });

        //Drag and move floating view using user's touch action.
        mFloatingView.findViewById(R.id.root_container).setOnTouchListener(new View.OnTouchListener() {
            private int initialX;
            private int initialY;
            private float initialTouchX;
            private float initialTouchY;

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:

                        //remember the initial position.
                        initialX = params.x;
                        initialY = params.y;

                        //get the touch location
                        initialTouchX = event.getRawX();
                        initialTouchY = event.getRawY();
                        return true;
                    case MotionEvent.ACTION_UP:
                        int Xdiff = (int) (event.getRawX() - initialTouchX);
                        int Ydiff = (int) (event.getRawY() - initialTouchY);

                        //The check for Xdiff <10 && YDiff< 10 because sometime elements moves a little while clicking.
                        //So that is click event.
                        if (Xdiff < 10 && Ydiff < 10) {
                            if (isViewCollapsed()) {
                                //When user clicks on the image view of the collapsed layout,
                                //visibility of the collapsed layout will be changed to "View.GONE"
                                //and expanded view will become visible.
                                collapsedView.setVisibility(View.GONE);
                                expandedView.setVisibility(View.VISIBLE);
                            }
                        }
                        return true;
                    case MotionEvent.ACTION_MOVE:
                        //Calculate the X and Y coordinates of the view.
                        params.x = initialX + (int) (event.getRawX() - initialTouchX);
                        params.y = initialY + (int) (event.getRawY() - initialTouchY);

                        //Update the layout with new X & Y coordinate
                        mWindowManager.updateViewLayout(mFloatingView, params);
                        return true;
                }
                return false;
            }
        });
    }

    /**
     * Detect if the floating view is collapsed or expanded.
     *
     * @return true if the floating view is collapsed.
     */
    private boolean isViewCollapsed() {
        return mFloatingView == null || mFloatingView.findViewById(R.id.collapse_view).getVisibility() == View.VISIBLE;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mFloatingView != null) mWindowManager.removeView(mFloatingView);
    }
}

然后添加。
  <service
            android:name="yourpakcage.FloatingService"
            android:enabled="true"
            android:exported="false"/>

layout_floating_widget.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <!--Root container-->
    <RelativeLayout
        android:id="@+id/root_container"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:ignore="UselessParent">

        <!--View while view is collapsed-->
        <RelativeLayout
            android:id="@+id/collapse_view"
            android:layout_width="wrap_content"
            android:visibility="visible"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <!--Icon of floating widget -->
            <ImageView
                android:id="@+id/collapsed_iv"
                android:layout_width="60dp"
                android:layout_height="60dp"
                android:layout_marginTop="8dp"
                android:src="@drawable/ic_android_circle"
                tools:ignore="ContentDescription"/>

            <!--Close button-->
            <ImageView
                android:id="@+id/close_btn"
                android:layout_width="20dp"
                android:layout_height="20dp"
                android:layout_marginLeft="40dp"
                android:src="@drawable/ic_close"
                tools:ignore="ContentDescription"/>
        </RelativeLayout>

        <!--View while view is expanded-->
        <LinearLayout
            android:id="@+id/expanded_container"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#F8BBD0"
            android:visibility="gone"
            android:orientation="horizontal"
            android:padding="8dp">

            <!--Album image for the song currently playing.-->
            <ImageView
                android:layout_width="80dp"
                android:layout_height="80dp"
                android:src="@drawable/music_player"
                tools:ignore="ContentDescription"/>

            <!--Previous button-->
            <ImageView
                android:id="@+id/prev_btn"
                android:layout_width="30dp"
                android:layout_height="30dp"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="20dp"
                android:src="@mipmap/ic_previous"
                tools:ignore="ContentDescription"/>

            <!--Play button-->
            <ImageView
                android:id="@+id/play_btn"
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="10dp"
                android:src="@mipmap/ic_play"
                tools:ignore="ContentDescription"/>

            <!--Next button-->
            <ImageView
                android:id="@+id/next_btn"
                android:layout_width="30dp"
                android:layout_height="30dp"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="10dp"
                android:src="@mipmap/ic_play_next"
                tools:ignore="ContentDescription"/>

            <RelativeLayout
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:orientation="vertical">

                <ImageView
                    android:id="@+id/close_button"
                    android:layout_width="20dp"
                    android:layout_height="20dp"
                    android:src="@drawable/ic_close"/>

                <ImageView
                    android:id="@+id/open_button"
                    android:layout_width="20dp"
                    android:layout_height="20dp"
                    android:layout_alignParentBottom="true"
                    android:src="@drawable/ic_open"/>
            </RelativeLayout>
        </LinearLayout>
    </RelativeLayout>
</FrameLayout>

请添加虚拟的可绘制对象和字符串。这将用于展示您的浮动视图。
startService(new Intent(passContext, FloatingViewService.class));

我无法检查差异,我几天前也遵循了同样的教程,上面是我的工作代码。 - Khemraj Sharma

1
很抱歉,您一直在遵循的指南已经过时了。 您正在尝试在后台启动服务,这会导致IllegalArgumentException,因为Android Oreo引入了 Background Execution Limits。 链接底部包含迁移指南。

在logcat中没有看到任何这样的异常抛出。同时,我正在Android 7上运行此应用程序。 - Pushkar
那么在这种情况下,恐怕你将需要解决2个问题。即使你为Android 7修复了当前代码中的问题,它也无法在Android 8或更高版本上运行。 - Sander
我添加了针对Android 8的检查,以使用startForegroundService()。一个问题解决了。 - Pushkar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接