「Android 自己设置 View」—— AreaSelectLayout
前几天写了一个小工具,其中一个设置项需要屏幕区域范围坐标参数,因为通过观察直接填写坐标信息不太现实,于是就有了通过直接拖拽屏幕去取这个参数的需求,又由于需要在任意界面都能选取,所以就想到了悬浮窗,这里以前写过一个悬浮窗工具类《FloatWindowUtils 实现及事件冲突处理详解》,想着再加少量手势及绘制应该就能实现,于是吭叽吭叽搞了半天,发现其中还是有些坑的,所以在此记录备忘。
效果图和用法如下:
未命名.gif
如上图,这个 View 的功能很简单,就是在屏幕上弹出一个全屏悬浮窗遮罩,而后在上面拖拽选择区域,最后保存坐标信息就好了。
详细实现我就不讲了,后面会贴源码,这里主要讲一下实现思路和几个需要注意的点
实现思路
继承 View 还是 ViewGroup
因为悬浮窗里需要有提醒文字以及取消和保存两个按钮,所以我没有直接去继承 View 来写,直接继承 View 来实现当然也可以,但是需要把下面的文字以及按钮都画出来,可能还需要内置按钮的触发回调等等业务,最终我选择了继承 ViewGroup(ConstrainsLayout) 来实现,这样做可以共享 ConstrainsLayout 的所有属性。和直接继承 View 实现相比,它的优点就是可以在布局文件中方便的增加子 View,缺点是集成度不够高,需要引用外部资源,所以具体实现可以看使用场景,这里没有复用的场景,集成度要求不高,所以就选择了更简单的继承 ViewGroup 来实现。
class AreaSelectLayout : ConstraintLayout { constructor(context: Context) : super(context) {} constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {} override fun onDraw(canvas: Canvas) { super.onDraw(canvas) } @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { return super.onTouchEvent(event) }}
<?xml version="1.0" encoding="utf-8"?><com.example.area_select_layout.AreaSelectLayout ... android:background="@color/half_trans"> <ImageButton android:id="@+id/btn_save" ... /> <ImageButton android:id="@+id/btn_cancel" ... /> <LinearLayout ...> <TextView android:id="@+id/textView4" android:text="拖动选取" ... /> <TextView android:id="@+id/textView5" android:text="起始" ... /> <TextView android:text="和" ... /> <TextView android:text="结束" ... /> <TextView android:text="区域" ... /> </LinearLayout></com.example.area_select_layout.AreaSelectLayout>
跟随手指移动绘制方框
区域选择,其实就是在屏幕上画对角线,手指落下,记录起点,手指移动,不断升级终点并通知 Canvas 画出来,手指抬起,保存最终坐标。这里需要注意的是 Y 坐标要减去 StatusBar 的高度。
@SuppressLint("ClickableViewAccessibility")override fun onTouchEvent(event: MotionEvent): Boolean { when (event.action) { MotionEvent.ACTION_DOWN -> { // 记录起点位置 setStartPos(event.rawX.toInt(), (event.rawY - statusBarHeight).toInt()) } MotionEvent.ACTION_MOVE -> { // 升级终点位置 setStopPos(event.rawX.toInt(), (event.rawY - statusBarHeight).toInt()) // 使布局失效,让系统触发 onDraw 重绘界面 invalidate() } } return super.onTouchEvent(event)}
位置存储以及方向判断
因为位置总共有四个点,八个坐标值,用变量来存很不便,所以我直接用 Rect 来存了,一是便于管理,可以用这些值方便的判断正在选区起始区域或者是结束区域,二是可以直接通过 Canvas.drawRect(Rect, Paint) 来绘制方框了。
private fun setStartPos(rawX: Int, rawY: Int) { // 起始区域已经绘制 if (startRect.bottom != 0) { startSelected = true } // 假如存在两个选区的话,则清屏重绘制 if (startRect.bottom != 0 && endRect.bottom != 0) { clear() } // 判断落点属于起始区域还是结束区域 if (startSelected) { end.x = rawX end.y = rawY } else { start.x = rawX start.y = rawY }}private fun setStopPos(rawX: Int, rawY: Int) { // 判断终点属于起始区域还是结束区域 if (startSelected) { // rawX < end.x 表示从左向右拖动 ? // ? 时 x 升级 right,y 同理 endRect.left = if (rawX < end.x) rawX else end.x // rawX > end.x 表示从右向左拖动 ? // ? 时 x 升级 left,y 同理 endRect.right = if (rawX > end.x) rawX else end.x endRect.top = if (rawY < end.y) rawY else end.y endRect.bottom = if (rawY > end.y) rawY else end.y } else { startRect.left = if (rawX < start.x) rawX else start.x startRect.right = if (rawX > start.x) rawX else start.x startRect.top = if (rawY < start.y) rawY else start.y startRect.bottom = if (rawY > start.y) rawY else start.y }}
使布局悬浮在应用之外
这块考虑了一下,最后还是没有用 FloatWindowUtils,由于就一个布局,理解了悬浮窗怎样玩之后,悬浮起一个布局也很简单,直接用 WindowManager 十几行代码搞定了。
private fun showSelectLayout() { if (!SystemSetings.isAppOpsOn(this)) { SystemSetings.openOpsSettings(this) Toast.makeText(this,"请先开启悬浮窗权限",Toast.LENGTH_SHORT).show() return } wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager val lp = WindowManager.LayoutParams() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY } else { lp.type = WindowManager.LayoutParams.TYPE_PHONE } lp.format = PixelFormat.TRANSLUCENT lp.flags = lp.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE lp.width = WindowManager.LayoutParams.MATCH_PARENT lp.height = WindowManager.LayoutParams.MATCH_PARENT lp.gravity = Gravity.END or Gravity.TOP if (selectLayout.parent!=null){ wm.removeView(selectLayout) } wm.addView(selectLayout, lp)}
核心部分基本就是上面这些了,算位置的时候略微有点绕但是不难,多分析下就出来了,下面贴下源码
项目源码
manifests.xml
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.area_select_layout"> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application></manifest>
MainActivity.kt
class MainActivity : AppCompatActivity() { private lateinit var selectLayout: AreaSelectLayout private lateinit var btnSave: View private lateinit var btnCancel: View private lateinit var wm: WindowManager @SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) selectLayout = View.inflate(this,R.layout.dialog_select_layout,null) as AreaSelectLay btnSave = selectLayout.findViewById(R.id.btn_save) btnCancel = selectLayout.findViewById(R.id.btn_cancel) btnSave.setOnClickListener { hideSelectLayout() val startRect = selectLayout.getStartRect() val endRect = selectLayout.getEndRect() textView.text = "start-area:[${startRect.left},${startRect.top}]," + "[${startRect.right},${startRect.bottom}]" + "\n" + "end-area:[${endRect.left},${endRect.top}]," + "[${endRect.right},${endRect.bottom}]" } btnCancel.setOnClickListener { hideSelectLayout() selectLayout.clear() } button.setOnClickListener { showSelectLayout() } } private fun hideSelectLayout() { wm.removeView(selectLayout) } private fun showSelectLayout() { if (!SystemSetings.isAppOpsOn(this)) { SystemSetings.openOpsSettings(this) Toast.makeText(this,"请先开启悬浮窗权限",Toast.LENGTH_SHORT).show() return } // 设置位置 wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager val lp = WindowManager.LayoutParams() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY } else { lp.type = WindowManager.LayoutParams.TYPE_PHONE } lp.format = PixelFormat.TRANSLUCENT lp.flags = lp.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE lp.width = WindowManager.LayoutParams.MATCH_PARENT lp.height = WindowManager.LayoutParams.MATCH_PARENT lp.gravity = Gravity.END or Gravity.TOP if (selectLayout.parent!=null){ wm.removeView(selectLayout) } wm.addView(selectLayout, lp) }}
AreaSelectLayout.kt
class AreaSelectLayout : ConstraintLayout { private var paint = Paint(Paint.ANTI_ALIAS_FLAG) private var endPaint = Paint(Paint.ANTI_ALIAS_FLAG) private var paintText = Paint(Paint.ANTI_ALIAS_FLAG) /** * 滑动范围 */ private var startRect = Rect() private var endRect = Rect() /** * 坐标文字边框 */ private var ltStrBounds = Rect() private var rbStrBounds = Rect() private var start = Point() private var end = Point() private var startSelected = false private val statusBarHeight: Int get() { val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") return resources.getDimensionPixelSize(resourceId) } constructor(context: Context) : super(context) {} constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {} init { paint.strokeWidth = dp2px(2f) paint.style = Paint.Style.STROKE paint.color = Color.parseColor("#1aad19") endPaint.strokeWidth = dp2px(2f) endPaint.style = Paint.Style.STROKE endPaint.color = Color.parseColor("#f45454") paintText.textSize = dp2px(12f) paintText.color = Color.parseColor("#1aad19") } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) // 绘制结束区域 canvas.drawRect(endRect, endPaint) // 绘制起始区域 canvas.drawRect(startRect, paint) } @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { when (event.action) { MotionEvent.ACTION_DOWN -> { setStartPos(event.rawX.toInt(), (event.rawY - statusBarHeight).toInt()) } MotionEvent.ACTION_MOVE -> { setStopPos(event.rawX.toInt(), (event.rawY - statusBarHeight).toInt()) invalidate() } } return super.onTouchEvent(event) } private fun setStartPos(rawX: Int, rawY: Int) { if (startRect.bottom != 0) { startSelected = true } if (startRect.bottom != 0 && endRect.bottom != 0) { clear() } if (startSelected) { end.x = rawX end.y = rawY } else { start.x = rawX start.y = rawY } } private fun setStopPos(rawX: Int, rawY: Int) { if (startSelected) { endRect.left = if (rawX < end.x) rawX else end.x endRect.right = if (rawX > end.x) rawX else end.x endRect.top = if (rawY < end.y) rawY else end.y endRect.bottom = if (rawY > end.y) rawY else end.y } else { startRect.left = if (rawX < start.x) rawX else start.x startRect.right = if (rawX > start.x) rawX else start.x startRect.top = if (rawY < start.y) rawY else start.y startRect.bottom = if (rawY > start.y) rawY else start.y } } fun clear() { startSelected = false startRect.top = 0 startRect.bottom = 0 startRect.left = 0 startRect.right = 0 endRect.top = 0 endRect.bottom = 0 endRect.left = 0 endRect.right = 0 invalidate() } fun setStartRect(rect: Rect) { this.startRect = rect } fun getStartRect(): Rect {// val rstRect = startRect// rstRect.top += statusBarHeight// rstRect.bottom += statusBarHeight return startRect } fun setEndRect(rect: Rect) { this.endRect = rect } fun getEndRect(): Rect {// val rstRect = endRect// rstRect.top += statusBarHeight// rstRect.bottom += statusBarHeight return endRect } fun dp2px(dp: Float): Float { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().displayMetrics) }}
SystemSetings.kt
/** * Created by skyrin on 2016/9/25. * 系统设置相关 */object SystemSetings { /** * 辅助服务能否开启 * @param context * @return true if Accessibility is on. */ fun isAccessibilitySettingsOn(context: Context): Boolean { var i: Int try { i = Settings.Secure.getInt(context.contentResolver, "accessibility_enabled") } catch (e: Settings.SettingNotFoundException) { Log.i("AccessibilitySettingsOn", e.message) i = 0 } if (i != 1) { return false } val string = Settings.Secure.getString(context.contentResolver, "enabled_accessibility_services") return string?.toLowerCase()?.contains(context.packageName.toLowerCase()) ?: false } /** * 打开辅助服务的设置 */ fun openAccessibilityServiceSettings(context: Context): Boolean { var result = true try { val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(intent) } catch (e: Exception) { result = false e.printStackTrace() } return result } /** 打开通知栏设置 */ @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1) fun openNotificationServiceSettings(context: Context) { try { val intent = Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS) context.startActivity(intent) } catch (e: Exception) { e.printStackTrace() } } /** * 打开app权限的设置 * @param context * @return */ fun openFloatWindowSettings(context: Context): Boolean { try { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) val uri = Uri.fromParts("package", context.packageName, null) intent.data = uri context.startActivity(intent) } catch (e: Exception) { e.printStackTrace() return false } return true } /** * 打开app权限的设置 * @param context * @return */ fun openFloatWindowSettings(context: Context, pkgName: String): Boolean { try { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) val uri = Uri.fromParts("package", pkgName, null) intent.data = uri context.startActivity(intent) } catch (e: Exception) { e.printStackTrace() return false } return true } /** * 打开悬浮窗设置页 * 部分第三方ROM无法直接跳转可使用[.openAppSettings]跳到应用介绍页 * * @param context * @return true if it's open successful. */ fun openOpsSettings(context: Context): Boolean { try { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.packageName)) context.startActivity(intent) } else { return openAppSettings(context) } } catch (e: Exception) { e.printStackTrace() return false } return true } /** * 打开应用介绍页 * * @param context * @return true if it's open success. */ fun openAppSettings(context: Context): Boolean { try { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) val uri = Uri.fromParts("package", context.packageName, null) intent.data = uri context.startActivity(intent) } catch (e: Exception) { e.printStackTrace() return false } return true } /** * 判断 悬浮窗口权限能否打开 * 因为android未提供直接跳转到悬浮窗设置页的api,此方法使用反射去查找相关函数进行跳转 * 部分第三方ROM可能不适用 * * @param context * @return true 允许 false禁止 */ fun isAppOpsOn(context: Context): Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return Settings.canDrawOverlays(context) } try { val `object` = context.getSystemService(Context.APP_OPS_SERVICE) ?: return false val localClass = `object`.javaClass val arrayOfClass = arrayOfNulls<Class<*>>(3) arrayOfClass[0] = Integer.TYPE arrayOfClass[1] = Integer.TYPE arrayOfClass[2] = String::class.java val method = localClass.getMethod("checkOp", *arrayOfClass) ?: return false val arrayOfObject1 = arrayOfNulls<Any>(3) arrayOfObject1[0] = 24 arrayOfObject1[1] = Binder.getCallingUid() arrayOfObject1[2] = context.packageName val m = method.invoke(`object`, *arrayOfObject1) as Int return m == AppOpsManager.MODE_ALLOWED } catch (ex: Exception) { ex.stackTrace } return false } /** * 启动app * * @param context * @param pkgName 包名 * @return 能否启动成功 */ fun startApp(context: Context, pkgName: String): Boolean { try { val manager = context.packageManager val openApp = manager.getLaunchIntentForPackage(pkgName) ?: return false openApp.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP context.startActivity(openApp) } catch (e: Exception) { return false } return true }}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?><android.support.constraint.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/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="160dp" android:text="选择区域" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.498" app:layout_constraintStart_toStartOf="parent" /> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="80dp" android:text="TextView" app:layout_constraintBottom_toTopOf="@+id/button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /></android.support.constraint.ConstraintLayout>
dialog_select_layout.xml
<?xml version="1.0" encoding="utf-8"?><com.example.area_select_layout.AreaSelectLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:background="@color/half_trans"> <ImageButton android:id="@+id/btn_save" android:layout_width="48dp" android:layout_height="48dp" android:background="@null" android:src="@drawable/ic_ok" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> <ImageButton android:id="@+id/btn_cancel" android:layout_width="48dp" android:layout_height="48dp" android:layout_marginBottom="0dp" android:background="@null" android:src="@drawable/ic_cancel" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" /> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="0dp" android:layout_marginEnd="8dp" android:orientation="horizontal" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/btn_save"> <TextView android:id="@+id/textView4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="拖动选取" android:textColor="@color/white" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toTopOf="@+id/btn_save" app:layout_constraintVertical_bias="0.448" tools:layout_editor_absoluteX="61dp" /> <TextView android:id="@+id/textView5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="起始" android:textColor="#1aad19" tools:layout_editor_absoluteX="162dp" tools:layout_editor_absoluteY="476dp" /> <TextView android:id="@+id/textView6" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="和" android:textColor="@color/white" tools:layout_editor_absoluteX="209dp" tools:layout_editor_absoluteY="475dp" /> <TextView android:id="@+id/textView7" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="结束" android:textColor="#f45454" tools:layout_editor_absoluteX="243dp" tools:layout_editor_absoluteY="474dp" /> <TextView android:id="@+id/textView8" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="区域" android:textColor="@color/white" tools:layout_editor_absoluteX="276dp" tools:layout_editor_absoluteY="474dp" /> </LinearLayout></com.example.area_select_layout.AreaSelectLayout>
colors.xml
<?xml version="1.0" encoding="utf-8"?><resources> <color name="colorPrimary">#008577</color> <color name="colorPrimaryDark">#00574B</color> <color name="colorAccent">#D81B60</color> <color name="red">#d02129</color> <color name="vip">#f5b53a</color> <color name="light_red">#ff620d</color> <color name="blue">#1189ff</color> <color name="blue_dark">#ff187998</color> <color name="white">#FFF</color> <color name="green">#ff1ce322</color> <color name="grey">#eeeffd</color> <color name="dark_gray">#525252</color> <color name="black">#000000</color> <color name="main">#09537a</color> <color name="main_trans">#991296db</color> <color name="full_trans">#00000000</color> <color name="no_trans">#00000001</color> <color name="half_trans">#80000000</color> <color name="status_bar">#09537a</color> <color name="df_layout">#fafafa</color> <color name="main_gray">#f5f5f5</color> <color name="waring">#ff620d</color> <color name="room_panel_bg">#80000000</color> <color name="sys_msg_bg">#b4b4b4</color> <color name="dialogue_msg">#3e3f3f</color> <color name="green0">#45C01A</color> <color name="green1">#45C01A</color> <color name="green2">#A3DEA3</color> <color name="green3">#1AAD19</color> <color name="ok">#1aad19</color> <color name="no">#ff620d</color> <color name="alpha_05_black">#0D000000</color> <color name="alpha_10_black">#1A000000</color> <color name="alpha_15_black">#26000000</color> <color name="alpha_20_black">#33000000</color> <color name="alpha_25_black">#40000000</color> <color name="alpha_30_black">#4D000000</color> <color name="alpha_33_black">#54000000</color> <color name="alpha_35_black">#59000000</color> <color name="alpha_40_black">#66000000</color> <color name="alpha_45_black">#73000000</color> <color name="alpha_50_black">#80000000</color> <color name="alpha_55_black">#8C000000</color> <color name="alpha_60_black">#99000000</color> <color name="alpha_65_black">#A6000000</color> <color name="alpha_70_black">#B3000000</color> <color name="alpha_75_black">#BF000000</color> <color name="alpha_80_black">#CC000000</color> <color name="alpha_85_black">#D9000000</color> <color name="alpha_90_black">#E6000000</color> <color name="alpha_95_black">#F2000000</color></resources>
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 「Android 自己设置 View」—— AreaSelectLayout