[3.3.0][M1] 360环视需求代码提交

This commit is contained in:
renwj
2023-06-13 16:39:27 +08:00
parent 59dd44a14b
commit dd9a4667df
37 changed files with 1388 additions and 231 deletions

View File

@@ -0,0 +1,307 @@
package com.mogo.eagle.core.utilcode.floating
import android.annotation.*
import android.app.Activity
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.util.Log
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.PopupWindow
import androidx.annotation.LayoutRes
import androidx.core.view.doOnAttach
import com.mogo.eagle.core.utilcode.kotlin.lifeCycleScope
import com.mogo.eagle.core.utilcode.util.*
import java.util.concurrent.atomic.AtomicBoolean
class MoGoPopWindow private constructor(builder: Builder){
private val content: View = builder.content ?: throw AssertionError("要填充的View不能为空")
private val pop: PopupWindow by lazy { PopupWindow(builder.width, builder.height) }
private val offsetX: Int = builder.offsetX
private var offsetY: Int = builder.offsetY
private var gravityInActivity: Int = builder.gravityInActivity
private val activity: Activity = builder.attachToActivity ?: throw AssertionError("要依附Activity实例不能为空")
private val isDraggable: Boolean = builder.isDraggable
private val isDismissOnTouchOutside: Boolean = builder.isDismissOnTouchOutside
private val isPendingShow by lazy { AtomicBoolean(false) }
private val onDismissed: (() -> Unit)? = builder.onDismissed
private val onShowed: (() -> Unit)? = builder.onShowed
private val onTouchInterceptorInDrag: ((child: View, event: MotionEvent) -> Boolean)? = builder.onTouchInterceptorInDrag
private val onViewAttachedToWindow: ((content: View) -> Unit)? = builder.onViewAttachedToWindow
@SuppressLint("ClickableViewAccessibility") fun show() {
if (isPendingShow.get() || pop.isShowing) {
return
}
isPendingShow.set(true)
activity.lifeCycleScope.launchWhenResumed {
val parent = activity.findViewById<View>(android.R.id.content) ?: throw AssertionError("附着的Activity上的ID为[android.R.id.content]的控件不存在.")
pop.contentView = content
pop.isAttachedInDecor = true
pop.setBackgroundDrawable(ColorDrawable())
content.doOnAttach {
onViewAttachedToWindow?.invoke(content)
onShowed?.invoke()
}
pop.setOnDismissListener {
onDismissed?.invoke()
}
pop.isTouchable = true
if (isDismissOnTouchOutside) {
pop.isFocusable = true
pop.isOutsideTouchable = true
} else {
pop.isFocusable= false
pop.isOutsideTouchable = false
}
if (isDraggable) {
val outer = Rect()
var oldRawX = 0f
var oldRawY = 0f
var isDownConsumed = false
pop.setTouchInterceptor { _, event ->
if (event == null) {
return@setTouchInterceptor false
}
val rawX = event.rawX
val rawY = event.rawY
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
val child = findChildInContent(content.parent as? ViewGroup, event.x, event.y) ?: content
Log.d(TAG, "child -> : ${child.javaClass.simpleName}")
if (onTouchInterceptorInDrag?.invoke(child, event) == true) {
isDownConsumed = true
return@setTouchInterceptor false
}
if (outer.isEmpty) {
val location = IntArray(2)
parent.getLocationOnScreen(location)
val left = location[0]
val top = location[1]
val right = left + parent.width
val bottom = top + parent.height
outer.set(left,top, right, bottom)
}
oldRawX = rawX
oldRawY = rawY
}
if (event.actionMasked == MotionEvent.ACTION_MOVE) {
if (!isDownConsumed) {
val dx = (rawX - oldRawX).toInt()
val dy = (rawY - oldRawY).toInt()
val params = content.rootView.layoutParams as WindowManager.LayoutParams
val oldX = params.x
val oldY = params.y
val newX = oldX + dx
val newY = oldY + dy
if (outer.contains(rawX.toInt(), rawY.toInt())) {
pop.update(newX, newY, content.width, content.height)
}
oldRawX = rawX
oldRawY = rawY
}
}
if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) {
if (isDownConsumed) {
isDownConsumed = false
}
}
false
}
}
pop.showAtLocation(parent, gravityInActivity, offsetX, offsetY)
}
}
private tailrec fun findChildInContent(group: ViewGroup?, x: Float, y: Float): View? {
if (group == null) {
return null
}
val count = group.childCount
if (count == 0) {
return null
}
for (i in (count - 1) downTo 0) {
val child = group.getChildAt(i)
val location = IntArray(2)
child.getLocationInWindow(location)
val rect = Rect(location[0], location[1], location[0] + child.width, location[1] + child.height)
val intX = x.toInt()
val intY = y.toInt()
if (child is ViewGroup) {
if (rect.contains(intX, intY)) {
return findChildInContent(child, x, y)
}
continue
}
if (rect.contains(intX, intY)) {
return child
}
}
return null
}
fun hide() {
if (!pop.isShowing) {
return
}
activity.lifeCycleScope.launchWhenResumed {
pop.dismiss()
}
}
fun isShowing(): Boolean = isPendingShow.get() || pop.isShowing
class Builder {
/**
* 内容控件实例,设置了此字段,不能再设置[layoutId]
*/
internal var content: View? = null
/**
* 内容控件的布局ID设置了此字段不能再设置[content]
*/
internal var layoutId: Int? = null
/**
* 当前窗口是否可拖拽
*/
internal var isDraggable: Boolean = false
/**
* 点击弹窗外部区域,是否消失
*/
internal var isDismissOnTouchOutside: Boolean = false
/**
* 依附的Activity实例
*/
internal var attachToActivity: Activity? = null
/**
* 相对于父控件X方向上的偏移量
*/
internal var offsetX: Int = 0
/**
* 相对于父控件Y方向上的偏移量
*/
internal var offsetY: Int = 0
/**
* 相对于父控件的布局位置
*/
internal var gravityInActivity: Int = Gravity.NO_GRAVITY
internal var width: Int = WindowManager.LayoutParams.WRAP_CONTENT
internal var height: Int = WindowManager.LayoutParams.WRAP_CONTENT
internal var onShowed:(()-> Unit)? = null
internal var onDismissed: (()-> Unit)? = null
/**
* 当处于拖拽状态时,是响应拖拽还是当前窗口自己的事件
*/
internal var onTouchInterceptorInDrag: ((childInContent: View, event: MotionEvent) -> Boolean)? = null
/**
* 当控件被attach到窗口的时候回调
*/
internal var onViewAttachedToWindow: ((content: View) -> Unit)? = null
fun contentView(content: View) = apply {
this.content = content
}
fun layoutId(@LayoutRes id: Int) = apply {
this.layoutId = id
}
fun draggable(draggable: Boolean) = apply {
this.isDraggable = draggable
}
fun isDismissOnTouchOutside(flag: Boolean) = apply {
this.isDismissOnTouchOutside = flag
}
fun attachToActivity(activity: Activity) = apply {
this.attachToActivity = activity
}
fun offsetX(x: Int) = apply {
this.offsetY = x
}
fun offsetY(y: Int) = apply {
this.offsetY = y
}
fun width(w: Int) = apply {
this.width = w
}
fun height(h: Int) = apply {
this.height = h
}
fun gravityInActivity(gravity: Int) = apply {
this.gravityInActivity = gravity
}
fun onTouchInterceptorInDrag(block: (childInContent: View, event: MotionEvent) -> Boolean) = apply {
this.onTouchInterceptorInDrag = block
}
fun onShowed(block: () -> Unit) = apply {
this.onShowed = block
}
fun onDismissed(block: () -> Unit) = apply {
this.onDismissed = block
}
fun onViewAttachedToWindow(block: (content: View) -> Unit) = apply {
this.onViewAttachedToWindow = block
}
fun build() : MoGoPopWindow {
val activity = attachToActivity ?: throw AssertionError("依附的activity不能为空")
if (content != null && layoutId != null) {
throw AssertionError("两者不能同时设置")
}
if (content == null && layoutId == null) {
throw AssertionError("两者不能都不设置,至少设置一个")
}
if (content == null) {
content = LayoutInflater.from(activity).inflate(layoutId!!, null)
}
if (content != null && content?.parent != null) {
throw AssertionError("内容控件不允许包含父控件.")
}
return MoGoPopWindow(this)
}
}
}

View File

@@ -0,0 +1,223 @@
package com.mogo.eagle.core.utilcode.mogo.floating
import android.annotation.*
import android.content.*
import android.content.res.*
import android.os.*
import android.util.*
import android.view.*
import android.widget.*
import androidx.annotation.*
abstract class AbsFloatingView: FrameLayout {
private var mPortraitY = 0f
private var mWindowWidth: Int = 0
private var mWindowHeight: Int = 0
private var mAnimator: AutoMoveEdgeAnimator? = null
/**
* 系统状态栏的高度
*/
private var mStatusBarHeight: Int = 0
private var mOldX: Float = 0f
private var mOldY: Float = 0f
private var mOldRawX: Float = 0f
private var mOldRawY: Float = 0f
companion object {
private const val MARGIN_EDGE = 13
}
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : this(context, attrs, defStyleAttr, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
isClickable = true
isFocusable = true
mStatusBarHeight = getStatusBarHeight()
}
@SuppressLint("InternalInsetResource")
private fun getStatusBarHeight(): Int {
val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")
if (resourceId > 0) {
return context.resources.getDimensionPixelSize(resourceId)
}
return 0
}
@CallSuper
override fun onAttachedToWindow() {
super.onAttachedToWindow()
val layoutId = getLayoutId()
if (layoutId == 0) {
throw AssertionError("view: ${this::class.java.simpleName}'s getLayoutId() return a invalid resID: 0")
}
val child = LayoutInflater.from(context).inflate(layoutId, this, true)
onChildInflated(child)
if (autoMoveToEdge()) {
mAnimator = AutoMoveEdgeAnimator()
}
}
@CallSuper
override fun onDetachedFromWindow() {
if (autoMoveToEdge()) {
mAnimator?.stop()
}
super.onDetachedFromWindow()
}
@SuppressLint("ClickableViewAccessibility")
final override fun onTouchEvent(event: MotionEvent?): Boolean {
if (!isDragEnabled()) {
return super.onTouchEvent(event)
}
if (event == null) {
return false
}
when(event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
mOldX = x
mOldY = y
mOldRawX = event.rawX
mOldRawY = event.rawY
updateSize()
if (autoMoveToEdge()) {
mAnimator?.stop()
}
}
MotionEvent.ACTION_MOVE -> {
updateViewPosition(event)
}
MotionEvent.ACTION_UP -> {
mPortraitY = 0f
if (autoMoveToEdge()) {
moveToEdge()
}
}
}
return true
}
private fun updateViewPosition(event: MotionEvent) {
var destX = mOldX + event.rawX - mOldRawX
if (!autoMoveToEdge()) {
if (destX < 0f) {
destX = 0f
} else {
val widthLeft = mWindowWidth - width
if (destX > widthLeft) {
destX = widthLeft.toFloat()
}
}
}
x = destX
var destY = mOldY + event.rawY - mOldRawY
if (destY < mStatusBarHeight) {
destY = mStatusBarHeight.toFloat()
}
val heightLeft = mWindowHeight - height
if (destY > heightLeft) {
destY = heightLeft.toFloat()
}
y = destY
}
private fun moveToEdge() {
moveToEdge(isNearestLeft(), context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)
}
private fun moveToEdge(isLeft: Boolean, isLandscape: Boolean) {
val moveDistance = if (isLeft) MARGIN_EDGE else mWindowWidth - MARGIN_EDGE
if (!isLandscape && mPortraitY != 0f) {
y = mPortraitY
mPortraitY = 0f
}
mAnimator?.start(moveDistance.toFloat(), 0f.coerceAtLeast(y).coerceAtMost((mWindowHeight - height).toFloat()))
}
private fun isNearestLeft(): Boolean {
val middle = mWindowWidth / 2
return x < middle
}
private fun updateSize() {
val group = parent as? ViewGroup ?: return
mWindowWidth = group.width
mWindowHeight = group.height
}
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
if (parent != null) {
val isLandscape = newConfig?.orientation == Configuration.ORIENTATION_LANDSCAPE
if (isLandscape) {
mPortraitY = y
}
post {
updateSize()
moveToEdge(isNearestLeft(), isLandscape)
}
}
}
@LayoutRes
abstract fun getLayoutId(): Int
protected open fun isDragEnabled(): Boolean = true
protected open fun autoMoveToEdge(): Boolean = false
protected open fun onChildInflated(child: View) { }
private fun move(deltaX: Float, deltaY: Float) {
x += deltaX
y += deltaY
}
private inner class AutoMoveEdgeAnimator: Runnable {
private val handler by lazy { Handler(Looper.getMainLooper()) }
private var destX: Float = 0f
private var destY: Float = 0f
private var startTime: Long = 0L
override fun run() {
if (rootView == null || rootView.parent == null) {
return
}
val progress = 1f.coerceAtMost((SystemClock.elapsedRealtime() - startTime) / 400f)
val deltaX = (destX - x) * progress
val deltaY = (destY - y) * progress
move(deltaX, deltaY)
if (progress < 1) {
handler.post(this)
}
}
fun start(x: Float, y: Float) {
this.destX = x
this.destY = y
startTime = SystemClock.elapsedRealtime()
handler.post(this)
}
fun stop() {
handler.removeCallbacks(this)
}
}
}

View File

@@ -2,4 +2,5 @@
<resources>
<item name="tag_click_time" type="id" />
<item name="view_lifecycle_owner" type="id" />
<item name="mogo_pop_window" type="id" />
</resources>