Develop/Java

[Java] 자바 android Floating Widget 구현(1)

JunJangE 2021. 7. 7. 15:01

android Floating Widget 구현을 자바로 알아보자.

우선 mainfests -> AndroidManifest.xml에서 앱을 다른 모든 앱 위에 표시할 수 있게 권한을 설정하고

Service를 시작하도록 호출하고 enabled를 true로 하여 활성화시킨다.

코드는 다음과 같다.

    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
   
     <service android:name=".WidgetService"
            android:enabled="true"/>

다음은 activity.main.xml로 가서 버튼을 간단하게 하나 만들어준다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <Button
        android:id="@+id/notify_me"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity에는 권한 설정과 버튼을 눌렀을 때 이벤트를 만든다.

주석을 보면서 코드 분석을 해보면 좋을 것 같다.

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


       // 버튼 정의
       Button button;
       button = (Button) findViewById(R.id.notify_me);

        // 권한을 확인한다.
        getpermission();

        // 버튼 클릭
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                if (!Settings.canDrawOverlays(MainActivity.this)){
                    getpermission();

                }else {
                    // 권한이 설정돼있으면 위젯 액티비티로 연결
                    Intent intent = new Intent(MainActivity.this, WidgetService.class);
                    startService(intent);

                    // 액티비티에 연결 후 MainActivity 종료
                    finish();

                }
            }
        });
    }

    // M 버전(안드로이드 6.0 마시멜로우 버전) 보다 같거나 큰 API에서만 설정창 이동 가능
    public void getpermission(){
        // 지금 창이 오버레이 설정창이 아니라면
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)){
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:"+getPackageName()));

            startActivityForResult(intent,1);

        }
    }

    // WidgetService 에서 처리된 결과를 받는 메소드
    // 처리된 결과 코드가 requestCode 를 판별해 결과 처리를 진행한다.
    // WidgetService 에서 처리 결과가 담겨온 데이터를 메시지로 보여준다.
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        // 권한 여부 확인
        if(requestCode == 1){
            // 권한을 사용할 수없는 경우 알림 표시
            if (!Settings.canDrawOverlays(MainActivity.this)){

                Toast.makeText(this, "Permission denied by user", Toast.LENGTH_SHORT).show();
            }
        }
    }
}

다음으로 새로운 activity를 하나 만들어준다.

여기서는 WidgetService라는 이름으로 activity를 하나 만들고 위젯 작동에 대한 이벤트를 만든다.

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.TextView;
import androidx.annotation.Nullable;
import java.util.Calendar;

public class WidgetService extends Service {

    int LAYOUT_FLAG;
    View mFloatingView;
    WindowManager windowManager;
    ImageView imageClose;
    TextView tvWidth;
    float height, width;



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



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

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){

            LAYOUT_FLAG = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;


        }else{

            LAYOUT_FLAG = WindowManager.LayoutParams.TYPE_PHONE;

        }

        // 우리가 만든 플로팅 뷰 레이아웃 확장
        mFloatingView = LayoutInflater.from(this).inflate(R.layout.layout_widget, null);

        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                LAYOUT_FLAG,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                PixelFormat.TRANSLUCENT);


        // 보기 위치 지정
        // 처음에는보기가 오른쪽 상단 모서리에 추가되며 필요에 따라 x-y 좌표를 변경
        layoutParams.gravity = Gravity.TOP|Gravity.RIGHT;
        layoutParams.x = 0;
        layoutParams.y = 100;

        WindowManager.LayoutParams imageParams = new WindowManager.LayoutParams( 140,
                140,
                LAYOUT_FLAG,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                PixelFormat.TRANSLUCENT);
        imageParams.gravity = Gravity.BOTTOM|Gravity.CENTER;
        imageParams.y = 100;


        windowManager = (WindowManager)getSystemService(WINDOW_SERVICE);
        imageClose = new ImageView(this);
        imageClose.setImageResource(R.drawable.close);
        imageClose.setVisibility(View.INVISIBLE);
        windowManager.addView(imageClose, imageParams);
        windowManager.addView(mFloatingView, layoutParams);
        mFloatingView.setVisibility(View.VISIBLE);

        height = windowManager.getDefaultDisplay().getHeight();
        width  = windowManager.getDefaultDisplay().getWidth();

        tvWidth = (TextView) mFloatingView.findViewById(R.id.imageView);



        // 사용자의 터치 동작을 사용하여 플로팅 뷰를 드래그하여 이동
        tvWidth.setOnTouchListener(new View.OnTouchListener() {

            int initialx, initialy;
            float initialTouchX, initialTouchY;
            long startCkickTime;

            // 클릭으로 볼 최대시간
            int MAX_CLICK_DURATION = 200;

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

                switch (motionEvent.getAction()){
                    case MotionEvent.ACTION_DOWN:

                        startCkickTime = Calendar.getInstance().getTimeInMillis();
                        imageClose.setVisibility(View.VISIBLE);

                        // 초기 위치 기억
                        initialx = layoutParams.x;
                        initialy = layoutParams.y;

                        //터치 위치 좌표 얻기
                        initialTouchX = motionEvent.getRawX();
                        initialTouchY = motionEvent.getRawY();

                        return true;

                    case MotionEvent.ACTION_UP:

                        long clickDuration = Calendar.getInstance().getTimeInMillis()-startCkickTime;
                        imageClose.setVisibility(view.GONE);


                        // 초기 좌표와 현재 좌표의 차이 가져 오기
                        layoutParams.x = initialx+(int) (initialTouchX-motionEvent.getRawX());
                        layoutParams.y = initialy+(int)(motionEvent.getRawY()-initialTouchY);

                        // 사용자가 플로팅 위젯을 제거 이미지로 끌어다 놓으면 서비스를 중지합니다.
                        if(clickDuration>=MAX_CLICK_DURATION)
                        {
                            // 제거 이미지 주변 거리
                            if (layoutParams.y>(height * 0.6 )){
                                stopSelf();
                            }

                        }


                        return true;

                    case MotionEvent.ACTION_MOVE:

                        // 초기 좌표와 현재 좌표의 차이 가져 오기
                        layoutParams.x = initialx+(int)(initialTouchX-motionEvent.getRawX());
                        layoutParams.y = initialy +(int) (motionEvent.getRawY()- initialTouchY);

                        // 새로운 X 및 Y 좌표로 레이아웃 업데이트
                        windowManager.updateViewLayout(mFloatingView, layoutParams);

                        if (layoutParams.y> (height * 0.6)){

                            imageClose.setImageResource(R.drawable.close);
                        }
                        else {
                            imageClose.setImageResource(R.drawable.close);
                        }
                        return true;

                }
                return false;
            }
        });



        return START_STICKY;
    }

    // 앱이 종료될때 실행
    @Override
    public void onDestroy(){
        super.onDestroy();

        if(mFloatingView != null){
            windowManager.removeView(mFloatingView);

        }
        if (imageClose != null){

            windowManager.removeView(imageClose);
        }

    }
}

여기서 이미지는 자신이 원하는 이미지로 쓰면 된다.


다음으로 위젯의 형태를 만들어보자.

새로운 레이아웃을 만들자. 여기서는 layout_widget.xml이라는 이름으로 만들었다.

코드는 다음과 같다.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <RelativeLayout
        android:id="@+id/layout_widget"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_margin="30dp">
        <TextView
            android:id="@+id/imageView"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:gravity="center"
            android:textColor="#ffffff"
            android:background="@drawable/pin"/>

    </RelativeLayout>

</RelativeLayout>

위 내용을 잘 따라왔다면 run을 눌러 잘 실행되는지 확인해본다.

위 코드를 실행하게 되면 다음 출력 화면과 같은 결과를 얻을 수 있다.

<결과화면>

+ 카카오톡 위젯을 확인해보면 위젯을 이동하고 놓았을 때 제일 가까운 사이드로 바로 붙어버리는데 나도 다시 공부한 후 양 사이드로 바로 붙어버리게 구현해봐야 할 것 같다. 그리고 클릭했을 때 앱의 정보가 나오거나 앱 홈 화면으로 다시 돌아가게도 구현하고 싶다.

참고

github

 

junjange/Java-Learning

자바 학습. Contribute to junjange/Java-Learning development by creating an account on GitHub.

github.com