안녕하세요 남갯입니다
오늘은 background floating button 을 만드는 방법에 대해 포스팅 해보려고 합니다.
background floating button 이란 페이스북 메신져와 비슷하게
이처럼 앱을 끄고나서도 floating button이 움직이는 것을 말합니다.
step : manifest
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"></uses-permission>
Alert permission 과 잠시후 만들 액티비티와 Service를 만듭니다.
<service
android:name=".bgfloating.FloatingService"
android:enabled="true"
android:permission="android.permission.SYSTEM_ALERT_WINDOW">
</service>
<activity android:name=".bgfloating.FloatingActivity"
android:launchMode="singleTop">
</activity>
step : FloatingActivity
class FloatingActivity : AppCompatActivity() {
private val ACTION_MANAGE_OVERLAY_PERMISSION_REQUEST_CODE = 1
val TAG = "Floating"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.e(TAG, "onCreate")
setContentView(R.layout.activity_floating)
checkPermission()
initTest()
}
fun initTest() {
helloWorld.setText("changed hello world")
}
override fun onPause() {
super.onPause()
Log.e(TAG, "onPause")
}
override fun onResume() {
super.onResume()
Log.e(TAG, "onResume")
}
override fun onStop() {
super.onStop()
Log.e(TAG, "onStop")
}
override fun onStart() {
super.onStart()
Log.e(TAG, "onStart")
}
fun checkPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // 마시멜로우 이상일 경우
if (!Settings.canDrawOverlays(this)) { // 체크
val intent = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:$packageName")
)
startActivityForResult(intent, ACTION_MANAGE_OVERLAY_PERMISSION_REQUEST_CODE)
} else {
Log.e("myLog", "startService")
startService()
}
} else {
startService()
}
}
fun startService() {
val floatingService = Intent(applicationContext, FloatingService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startService(floatingService)
}
}
fun sttopService() {
stopService(Intent(applicationContext, FloatingService::class.java))
}
@TargetApi(Build.VERSION_CODES.M)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == ACTION_MANAGE_OVERLAY_PERMISSION_REQUEST_CODE) {
if (!Settings.canDrawOverlays(this)) {
// TODO 동의를 얻지 못했을 경우의 처리
finish()
} else {
startService()
}
}
}
override fun onDestroy() {
super.onDestroy()
// sttopService()
}
}
먼져 액티비티를 하나 만들어줍니다.
여기서 저는 service를 시작하자마자 보여지도록 되어있는데,
* pause 상태에만 보여주려면?
* View에서 hide 메소드를 통해 원하는 상황에서 안보이게 해주시면 됩니다.
pause 시점에만 보여주도록 하시려면??
bindService를 통해 바인드합니다.
private lateinit var floatingService: FloatingService
val onServiceConnection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as FloatingService.LocalBinder
floatingService = binder.getService()
}
}
그후 service에 floatingService에 show, hide를 통해 호출해주시면 됩니다.
step : FloatingService
* 여기서 중점적으로 봐야할것은 M부터는
오버레이 권한을 받아야합니다.
* O버젼부터는 Background로 돌아갈경우 foreground 형태로 noti를 띄워주어야하지만 floating button이 있기때문에 자동으로 foreground로 인식하게 됩니다.
class FloatingService : Service() {
lateinit var floatingHeadWindow: FloatingHeadWindow
private val mBinder = LocalBinder()
override fun onBind(intent: Intent?): IBinder? {
return mBinder
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.e("myLog", "onStartCommand")
init()
return super.onStartCommand(intent, flags, startId)
}
fun init() {
Log.e("myLog", "init")
if (!::floatingHeadWindow.isInitialized) {
Log.e("myLog", "is not initalized")
floatingHeadWindow = FloatingHeadWindow(applicationContext).apply {
create()
createLayoutParams()
show()
}
}
}
override fun onCreate() {
super.onCreate()
Log.e("myLog", "onCreate")
}
inner class LocalBinder : Binder() {
fun getService(): FloatingService {
return this@FloatingService
}
}
override fun onDestroy() {
super.onDestroy()
floatingHeadWindow.hide()
Log.e("myLog", "onDestroy")
}
}
서비스는 이렇게 돌리게 되는데
위의 예제처럼
BindService로 돌렸을경우 서비스의 생명주기에 따라 init에
Oncreate에 넣어주시면 됩니다. (이건 서비스 부분이니 찾아서 해보시길 바랍니다.)
step : FloatingView
그리고 저희가 표현할 FloatingView를 만듭니다.
package com.namget.customline.bgfloating
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.widget.FrameLayout
class FloatingView @JvmOverloads constructor(
val mContext: Context,
val attributeSet: AttributeSet? = null,
val def: Int = 0
) :
FrameLayout(mContext, attributeSet, def), View.OnTouchListener {
private val CLICK_DRAG_TOLERANCE =
10f // Often, there will be a slight, unintentional, drag when the user taps the FAB, so we need to account for this.
private var downRawX: Int = 0
private var downRawY: Int = 0
private var lastX: Int = 0
private var lastY: Int = 0
private lateinit var callbacks: Callbacks
private val mGestureDetector: GestureDetector
private var mTouchSlop: Int
init {
setOnTouchListener(this)
mGestureDetector = GestureDetector(context, GestureListener())
mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
}
fun setCallbacks(callbacks: Callbacks) {
this.callbacks = callbacks
}
override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
val layoutParams = view.layoutParams
mGestureDetector.onTouchEvent(motionEvent)
val action = motionEvent.action
val x = motionEvent.rawX.toInt()
val y = motionEvent.rawY.toInt()
if (action == MotionEvent.ACTION_DOWN) {
downRawX = x
downRawY = y
lastX = x
lastY = y
return true // Consumed
} else if (action == MotionEvent.ACTION_MOVE) {
val nx = (x - lastX)
val ny = (y - lastY)
lastX = x
lastY = y
callbacks.onDrag(nx, ny)
return true // Consumed
} else if (action == MotionEvent.ACTION_UP) {
Log.e("action", "ACTION_UP")
callbacks.onDragEnd(x, y)
return true
} else {
return super.onTouchEvent(motionEvent)
}
}
inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent?): Boolean {
Log.e("gesture", "onClick")
callbacks.onClick()
return true
}
}
interface Callbacks {
fun onDrag(dx: Int, dy: Int)
fun onDragEnd(dx: Int, dy: Int)
fun onDragStart(dx: Int, dy: Int)
fun onClick()
}
}
이렇게 말이죠
여기서 GestureDetector를 통한 Callbacks는 floatingView의 이벤트를 Window에 전달하는 역할을 하게됩니다.
step : FloatingHeadWindow
class FloatingHeadWindow(val context: Context) : FloatingView.Callbacks {
private var mWindowManager: WindowManager =
context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
private lateinit var mLayoutParams: WindowManager.LayoutParams
private lateinit var mView: View
private var mViewAdded = false
private lateinit var rect: Rect
private lateinit var mAnimator: ObjectAnimator
fun show() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Settings.canDrawOverlays(context)) {
mWindowManager.addView(mView, mLayoutParams)
}
} else {
mWindowManager.addView(mView, mLayoutParams)
}
mViewAdded = true
}
fun hide() {
mWindowManager.removeView(mView)
mViewAdded = false
}
fun create() {
if (!mViewAdded) {
mView = LayoutInflater.from(context).inflate(R.layout.item_floating, null, false)
(mView as FloatingView).setCallbacks(this)
rect = Rect()
updateScreenLimit(rect)
mAnimator = ObjectAnimator.ofPropertyValuesHolder(this)
mAnimator.interpolator = DecelerateInterpolator(1.0f)
}
}
fun updateScreenLimit(rect: Rect) {
val dm = context.resources.displayMetrics
rect.left = -30
rect.right = dm.widthPixels - getWidth()
rect.top = 30
rect.bottom = dm.heightPixels - getHeight()
}
fun magHorizontal(x: Int): Int {
var x = x
val dm = context.resources.displayMetrics
if (x + 78 < 0) {
x = rect.left
} else if (x + getWidth() - 78 > dm.widthPixels) {
x = rect.right
}
return x
}
fun magVertical(y: Int): Int {
var y = y
if (y < rect.top) {
y = rect.top
} else if (y > rect.bottom) {
y = rect.bottom
}
return y
}
fun getX(): Int = mLayoutParams.x
fun getY(): Int = mLayoutParams.y
fun settle() {
Log.e("settle", "before X ${getX()}")
Log.e("settle", "before Y ${getY()}")
val x = magHorizontal(getX())
val y = magVertical(getY())
Log.e("settle", "" + x)
Log.e("settle", "" + y)
animateTo(x, y)
}
fun animateTo(x: Int, y: Int) {
val xHolder = PropertyValuesHolder.ofInt("x", x)
val yHolder = PropertyValuesHolder.ofInt("y", y)
Log.e("animateTo", "x holder $xHolder")
Log.e("animateTo", "y holder $yHolder")
mAnimator.setValues(xHolder, yHolder)
mAnimator.duration = 200
mAnimator.start()
mAnimator.addListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {
}
override fun onAnimationEnd(animation: Animator?) {
Log.e("AnimatorListener", "onAnimationEnd")
}
override fun onAnimationCancel(animation: Animator?) {
Log.e("AnimatorListener", "onAnimationCancel")
}
override fun onAnimationStart(animation: Animator?) {
Log.e("AnimatorListener", "onAnimationStart")
}
})
}
fun createLayoutParams() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mLayoutParams = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
or WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR,
PixelFormat.TRANSLUCENT
)
} else {
mLayoutParams = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
or WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR),
PixelFormat.TRANSLUCENT
)
}
mLayoutParams.gravity = Gravity.TOP or Gravity.LEFT
mLayoutParams.x = -mView.width
mLayoutParams.y = -mView.height
}
fun moveTo(pos: Point) {
moveTo(pos.x, pos.y)
}
fun moveTo(x: Int, y: Int) {
if ((mView != null) and (mViewAdded)) {
Log.e("test Int", "x : $x y : $y")
mLayoutParams.x = x
mLayoutParams.y = y
mWindowManager.updateViewLayout(mView, mLayoutParams)
}
}
fun moveBy(dx: Int, dy: Int) {
if (mView != null && mViewAdded) {
Log.e("test Int", "dx : $dx dy : $dy")
mLayoutParams.x += dx
mLayoutParams.y += dy
mWindowManager.updateViewLayout(mView, mLayoutParams)
}
}
fun getWidth(): Int = mView.measuredWidth
fun getHeight(): Int = mView.measuredHeight
override fun onDrag(dx: Int, dy: Int) {
Log.e("test Float", "dx : $dx dy : $dy")
moveBy(dx, dy)
}
override fun onDragEnd(dx: Int, dy: Int) {
settle()
}
override fun onDragStart(dx: Int, dy: Int) {
}
override fun onClick() {
startActivity()
}
private fun startActivity() {
try {
// 알림 팝업용 펜딩 인덴트
val contentIntent = PendingIntent.getActivity(
context,
9999,
Intent(context, FloatingActivity::class.java)
.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK
or Intent.FLAG_ACTIVITY_SINGLE_TOP
or Intent.FLAG_ACTIVITY_NO_USER_ACTION
or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
),
//.putExtras(pushIntent),
PendingIntent.FLAG_ONE_SHOT
)
contentIntent.send()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
- create 메소드를 통해 이전에 생성했던 FloatingView를 만들고 moveTo 메소드를 통해 플로팅을 이동시키는것입니다.
startActivity는 플로팅버튼을 클릭했을때 어떤 액티비티를 실행시킬지에 대한 내용입니다.
show와 hide를 상황에 맞게 쓰면 됩니다.
'IT > 안드로이드 관련' 카테고리의 다른 글
[안드로이드] 둥근 모서리 레이아웃 child view (0) | 2019.07.17 |
---|---|
[안드로이드] 개행과 색상을 넣는법 (0) | 2019.06.24 |
[안드로이드] ClipData.Item.getUri() 해결하기 (1) | 2019.06.18 |
[안드로이드] AutoCompleteTextView 자동 검색 기능 만들기 (5) | 2019.05.28 |
[안드로이드] html fromHtml deprecated (0) | 2019.05.27 |