본문 바로가기
FireMonkey 예제 자료

안드로이드 FireMonkey TextView Floating 위젯 파스칼 언어 예제

by kimsyo11 2022. 6. 26.
728x90

 

 

아래의 파일은 제가 파스칼 언어로 제작한 플로팅 위젯 입니다

 

기존에는 자바로 코딩하여 플로팅위젯을 이용하였지만 불편한 사람을 위해 파스칼로 다시 짰습니다.

 

꼭 이용하실 경우에는 권한중 다른앱 그리기 허용해주셔야 합니다.

 <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

KMSFloatingWidget.pas
0.00MB

 

자바코드 ↓

package com.KMSLyric;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.widget.Toast;
import android.app.Service;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.os.IBinder;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.WindowManager;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;

public class KMSLyricService extends Service {
    private TextView mPopupView; //항상 보이게 할 뷰
    private WindowManager.LayoutParams mParams; //layout params 객체. 뷰의 위치 및 크기를 지정하는 객체
    private WindowManager mWindowManager; //윈도우 매니저
    private SeekBar mSeekBar; //투명도 조절 seek bar
	
	public static final String KMS_Lyric = "KMS_Lyric";
    Intent KMSIntent = new Intent("com.KMSLyric.KMSLyricService");
	String KMSMessage;
	
	private float START_X,
    START_Y; //움직이기 위해 터치한 시작 점
    private int PREV_X,
    PREV_Y; //움직이기 이전에 뷰가 위치한 점
    private int MAX_X = -1,
    MAX_Y = -1; //뷰의 위치 최대 값

    private OnTouchListener mViewTouchListener = new OnTouchListener() {
         @Override public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: //사용자 터치 다운이면
                if (MAX_X == -1)
                    setMaxPosition();
                START_X = event.getRawX(); //터치 시작 점
                START_Y = event.getRawY(); //터치 시작 점
                PREV_X = mParams.x; //뷰의 시작 점
                PREV_Y = mParams.y; //뷰의 시작 점
                break;
            case MotionEvent.ACTION_MOVE:
                int x = (int)(event.getRawX() - START_X); //이동한 거리
                int y = (int)(event.getRawY() - START_Y); //이동한 거리

                //터치해서 이동한 만큼 이동 시킨다
                mParams.x = PREV_X + x;
                mParams.y = PREV_Y + y;

                optimizePosition(); //뷰의 위치 최적화
                mWindowManager.updateViewLayout(mPopupView, mParams); //뷰 업데이트
                break;
            }

            return true;
        }
    };

     @Override
    public IBinder onBind(Intent arg0) {
        return null;
    }
	
	 @Override
    public void onTaskRemoved(Intent rootIntent) { //핸들링 하는 부분
		Toast.makeText(this, "KMSPack 강제종료 감지", Toast.LENGTH_SHORT).show();
        stopSelf(); //서비스 종료
    }
	
	private BroadcastReceiver mMessageReceiver = new BroadcastReceiver() { 
	@Override public void onReceive(Context context, Intent intent) { 
	// TODO Auto-generated method stub 
	// Get extra data included in the Intent 
	if(intent.getAction().equals("com.KMSLyric.KMSLyricService")){
	KMSMessage = intent.getStringExtra("KMS_Lyric"); 
	mPopupView.setText(KMSMessage);
	mWindowManager.updateViewLayout(mPopupView, mParams); 
			}
		} 
	};

     @Override
    public void onCreate() {
        super.onCreate();
		KMSIntent.putExtra("KMS_Lyric","KMSPack 플로팅 가사창 입니다.");
		sendBroadcast(KMSIntent);

		final IntentFilter theFilter = new IntentFilter();
        theFilter.addAction("com.KMSLyric.KMSLyricService");
		registerReceiver(mMessageReceiver, theFilter);

        mPopupView = new TextView(this); //뷰 생성
        mPopupView.setText(KMSIntent.getStringExtra("KMS_Lyric")); //텍스트 설정
        mPopupView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); //텍스트 크기 18sp
        mPopupView.setTextColor(Color.GREEN); //글자 색상
        mPopupView.setBackgroundColor(Color.GRAY); //텍스트뷰 배경 색
		//mPopupView.setBackgroundColor(Color.argb(127, 0, 255, 255)); //텍스트뷰 배경 색

        mPopupView.setOnTouchListener(mViewTouchListener); //팝업뷰에 터치 리스너 등록

        //최상위 윈도우에 넣기 위한 설정
        mParams = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.TYPE_PHONE, //항상 최 상위에 있게. status bar 밑에 있음. 터치 이벤트 받을 수 있음.
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, //이 속성을 안주면 터치 & 키 이벤트도 먹게 된다.
                //포커스를 안줘서 자기 영역 밖터치는 인식 안하고 키이벤트를 사용하지 않게 설정
                PixelFormat.TRANSLUCENT); //투명
        mParams.gravity = Gravity.LEFT | Gravity.TOP; //왼쪽 상단에 위치하게 함.

        mWindowManager = (WindowManager)getSystemService(WINDOW_SERVICE); //윈도우 매니저 불러옴.
        mWindowManager.addView(mPopupView, mParams); //최상위 윈도우에 뷰 넣기. *중요 : 여기에 permission을 미리 설정해 두어야 한다. 매니페스트에

        addOpacityController(); //팝업 뷰의 투명도 조절하는 컨트롤러 추가
    }

	/*
	@Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // 서비스가 호출될 때마다 실행
        return super.onStartCommand(intent, flags, startId);
    }
	*/

    /**
     * 뷰의 위치가 화면 안에 있게 최대값을 설정한다
     */
    private void setMaxPosition() {
        DisplayMetrics matrix = new DisplayMetrics();
        mWindowManager.getDefaultDisplay().getMetrics(matrix); //화면 정보를 가져와서

        MAX_X = matrix.widthPixels - mPopupView.getWidth(); //x 최대값 설정
        MAX_Y = matrix.heightPixels - mPopupView.getHeight(); //y 최대값 설정
    }

    /**
     * 뷰의 위치가 화면 안에 있게 하기 위해서 검사하고 수정한다.
     */
    private void optimizePosition() {
        //최대값 넘어가지 않게 설정
        if (mParams.x > MAX_X)
            mParams.x = MAX_X;
        if (mParams.y > MAX_Y)
            mParams.y = MAX_Y;
        if (mParams.x < 0)
            mParams.x = 0;
        if (mParams.y < 0)
            mParams.y = 0;
    }

    /**
     * 알파값 조절하는 컨트롤러를 추가한다
     */
    private void addOpacityController() {
        mSeekBar = new SeekBar(this); //투명도 조절 seek bar
        mSeekBar.setMax(100); //맥스 값 설정.
        mSeekBar.setProgress(100); //현재 투명도 설정. 100:불투명, 0은 완전 투명
        mSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
             @Override public void onStopTrackingTouch(SeekBar seekBar) {}
             @Override public void onStartTrackingTouch(SeekBar seekBar) {}

             @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                mParams.alpha = progress / 100.0f; //알파값 설정
                mWindowManager.updateViewLayout(mPopupView, mParams); //팝업 뷰 업데이트
            }
        });

        //최상위 윈도우에 넣기 위한 설정
        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.MATCH_PARENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.TYPE_PHONE, //항상 최 상위에 있게. status bar 밑에 있음. 터치 이벤트 받을 수 있음.
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, //이 속성을 안주면 터치 & 키 이벤트도 먹게 된다.
                //포커스를 안줘서 자기 영역 밖터치는 인식 안하고 키이벤트를 사용하지 않게 설정
                PixelFormat.TRANSLUCENT); //투명
        params.gravity = Gravity.LEFT | Gravity.TOP; //왼쪽 상단에 위치하게 함.

        mWindowManager.addView(mSeekBar, params);
    }

    /**
     * 가로 / 세로 모드 변경 시 최대값 다시 설정해 주어야 함.
     */
     @Override
    public void onConfigurationChanged(Configuration newConfig) {
        setMaxPosition(); //최대값 다시 설정
        optimizePosition(); //뷰 위치 최적화
    }

     @Override
    public void onDestroy() {
        if (mWindowManager != null) { //서비스 종료시 뷰 제거. *중요 : 뷰를 꼭 제거 해야함.
            if (mPopupView != null)
                mWindowManager.removeView(mPopupView);
            if (mSeekBar != null)
                mWindowManager.removeView(mSeekBar);
        }
        super.onDestroy();
    }
}

 

파스칼 코드↓

unit KMSFloatingWidget;

///////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// 네이버 카페 : https://cafe.naver.com/soyeoncode     ////////////////////////////////////////////////////
// 코딩 작성자 : kimsyo5140@naver.com                  ////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////

interface

uses
  Androidapi.JNI.Support, Androidapi.Helpers,
  System.Threading, Androidapi.JNI.Widget, Androidapi.JNI.Util, Androidapi.JNI.GraphicsContentViewText,
  Androidapi.JNIBridge, Androidapi.JNI.JavaTypes;

procedure FloatingWidgetCreate;
procedure FloatingonDestroy;
procedure setMaxPosition;
procedure optimizePosition;  //최대값 넘어가지 않게 설정

 type
 KMSTouchEvent = class(TJavaLocal, JView_OnTouchListener)
  private
  public
    constructor Create;
    function onTouch(v: JView; event: JMotionEvent): Boolean; cdecl;
  end;
var   mViewTouchListener : KMSTouchEvent;
      START_X, START_Y : Single; //움직이기 위한 터치 시작점
      PREV_X, PREV_Y : Integer; //움직이기 이전에 뷰가 위치한 점
      MAX_X,  MAX_Y : Integer;  //뷰의 위치 최대 값
      WindowManagerObject : JObject;
      mWindowManager : JWindowManager;
      mParams : JWindowManager_LayoutParams;
      mPopupView : JTextView;

implementation

procedure setMaxPosition;
var matrix : JDisplayMetrics;
begin
  matrix := TJDisplayMetrics.JavaClass.init;
  mWindowManager.getDefaultDisplay.getMetrics(matrix);
  MAX_X := matrix.widthPixels - mPopupView.getWidth; //x 최대값 설정
  MAX_Y := matrix.heightPixels - mPopupView.getHeight; //y 최대값 설정
end;

procedure optimizePosition;  //최대값 넘어가지 않게 설정
begin
  if (mParams.x > MAX_X) then
      mParams.x := MAX_X;
  if (mParams.y > MAX_Y) then
      mParams.y := MAX_Y;
  if (mParams.x < 0) then
      mParams.x := 0;
  if (mParams.y < 0) then
      mParams.y := 0;
end;

constructor KMSTouchEvent.Create;
begin
  inherited Create;
end;
function KMSTouchEvent.onTouch(v: JView; event: JMotionEvent): Boolean; cdecl;
begin
  if event.getAction = TJMotionEvent.JavaClass.ACTION_DOWN then
  begin
    if MAX_X = -1 then
      setMaxPosition;
    START_X := event.getRawX(); //터치 시작 점
    START_Y := event.getRawY(); //터치 시작 점
    PREV_X := mParams.x; //뷰의 시작 점
    PREV_Y := mParams.y; //뷰의 시작 점
  end;
  if event.getAction = TJMotionEvent.JavaClass.ACTION_MOVE then
  begin
    var x : integer := Trunc(event.getRawX() - START_X); //이동한 거리
    var y : integer := Trunc(event.getRawY() - START_Y); //이동한 거리
     //터치해서 이동한 만큼 이동 시킨다
    mParams.x := PREV_X + x;
    mParams.y := PREV_Y + y;
    optimizePosition; //뷰의 위치 최적화
    mWindowManager.updateViewLayout(mPopupView, mParams); //뷰 업데이트
  end;
end;

procedure FloatingWidgetCreate;
begin
  mViewTouchListener := KMSTouchEvent.Create;
  MAX_X := -1;
  MAX_Y := MAX_X;
  mPopupView := TJTextView.JavaClass.init(SharedActivityContext); //뷰 생성
  mPopupView.setText(StrToJCharSequence('수질 위젯' + #13#10 + '(수신대기)')); //텍스트 설정
  mPopupView.setTextSize(TJTypedValue.JavaClass.COMPLEX_UNIT_SP, 14); //텍스트 크기 18sp
  mPopupView.setTextColor(TJColor.JavaClass.GREEN);  //글자 색상
  mPopupView.setBackgroundColor(TJColor.JavaClass.GRAY); //텍스트뷰 배경 색
  mPopupView.setOnTouchListener(mViewTouchListener);
  mParams := TJWindowManager_LayoutParams.JavaClass.init(
  TJWindowManager_LayoutParams.JavaClass.WRAP_CONTENT,
  TJWindowManager_LayoutParams.JavaClass.WRAP_CONTENT,
  TJWindowManager_LayoutParams.JavaClass.TYPE_APPLICATION_OVERLAY, //항상 최 상위에 있게. status bar 밑에 있음. 터치 이벤트 받을 수 있음.
  TJWindowManager_LayoutParams.JavaClass.FLAG_NOT_FOCUSABLE, //이 속성을 안주면 터치 & 키 이벤트도 먹게 된다.
  TJPixelFormat.JavaClass.TRANSLUCENT);  //TRANSLUCENT = 투명
  mParams.gravity := TJGravity.JavaClass.LEFT or TJGravity.JavaClass.TOP; //왼쪽 상단에 위치하게 함.
  WindowManagerObject := SharedActivityContext.getSystemService(TJContext.JavaClass.WINDOW_SERVICE);
  mWindowManager := TJWindowManager.Wrap((WindowManagerObject as ILocalObject).GetObjectID);
  mWindowManager.addView(mPopupView, mParams);
end;

procedure FloatingonDestroy;
begin
  if (mWindowManager <> nil) then
  begin
    if (mPopupView <> nil) then
      mWindowManager.removeView(mPopupView);
  end;
end;

end.

 

 

1. 액티비티에 이용하실 경우 ↓↓↓↓↓↓↓↓↓↓↓ 

uses문에  KMSFloatingWidget 추가 해주시고

예시로 아래처럼 사용하시면 됩니다

uses KMSFloatingWidget;

procedure TForm1.Button1Click(Sender: TObject);
begin
  FloatingWidgetCreate// 플로팅 위젯 생성 표시	
end;

procedure TextViewSetText(Tmpstr : string);
begin
  mPopupView.setText(StrToJCharSequence(Tmpstr)); //텍스트뷰의 내용 설정
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FloatingonDestroy;  //플로팅 위젯 종료
end;

 

 

파이어몽키 안드로이드 서비스에 플로팅 위젯 서비스를 이용하실 경우 

우선은 서비스를 포그라운드 서비스를 구현 하셔야 합니다 포그라운드 서비스 구현 안하고 이용하시는 방법은

 

매니페스트의 파일에서 SDK버전을 25로 변경해주시면 플로팅 위젯은 이용은 가능하나
반대로 서비스쪽은 불안정 할수가 있습니다

 

2. 일반적인 서비스에서 구동하는 방법입니다 ↓↓↓↓↓↓↓↓↓↓↓

//TDM의 폼에 이벤트 항목에 AndroidServiceConfigurationChanged 더블클릭하면 아래처럼 코드 나오며 
//setMaxPosition 와 optimizePosition를 넣어주시면 됩니다.


uses KMSFloatingWidget; 

procedure TextViewSetText(Tmpstr : string);
begin
  mPopupView.setText(StrToJCharSequence(Tmpstr)); //텍스트뷰의 내용 설정
end;

function TDM.AndroidServiceStartCommand(const Sender: TObject;
  const Intent: JIntent; Flags, StartId: Integer): Integer;
begin
  FloatingWidgetCreate;
end;

procedure TDM.AndroidServiceConfigurationChanged(const Sender: TObject;
  const NewConfig: JConfiguration);
begin
  setMaxPosition();
  optimizePosition();
end;

 

 

3. 서비스 쓰레드 사용하실 경우 ↓↓↓↓↓↓↓↓↓↓↓

 

서비스 내에 쓰레드 사용하시면 플로팅 텍스트 내용을 변경하실 경우에는 예시로 아래처럼 이용해주셔야 합니다

서비스 쓰레드 안사용하실경우 아래처럼 사용하실 필요가 없습니다.

쓰레드내에 아래처럼 처리 안하실경우 서비스 멈춤현상 일어나게 되며 정상적인 서비스가 이용이 안되니 

꼭 쓰레드내에   mPopupView.setText사용하실 경우 아래처럼 해주셔야합니다

uses KMSFloatingWidget;

JTextRunnable = class(TJavaLocal, JRunnable)
  private
    TextTmpStr : string;
  public
    constructor Create;
    procedure run; cdecl;
  end;

var TextRunnable : JTextRunnable;
	TextHandle : JHandler;

constructor JTextRunnable.Create;
begin
  inherited Create;
end;

procedure JTextRunnable.run;
begin
  mPopupView.setText(StrToJCharSequence(TextTmpStr));
end;

procedure ThreadSetTextView;
begin
  TextHandle.post(TextRunnable);
end;

function TDM.AndroidServiceStartCommand(const Sender: TObject;
  const Intent: JIntent; Flags, StartId: Integer): Integer;
begin
  TextHandle := TJHandler.JavaClass.init(TJLooper.JavaClass.getMainLooper); //  텍스트 뷰 핸들 생성
  TextRunnable := JTextRunnable.Create;                                    //  텍스트뷰 Runnable 생성
  FloatingWidgetCreate;
  TTask.Run(procedure
  begin
    while True do
    begin
      sleep(300000); //300000
 		TextRunnable.TextTmpStr := 'ㅇㄹㅇㄹ';
		ThreadSetTextView;
        Exit;
    end;
  end).Start;
end;

procedure TDM.AndroidServiceDestroy(Sender: TObject);
begin
  FloatingonDestroy;
end;
728x90

댓글