From 5105e6638857cd973f7f1e8aa87ff5743d41089d Mon Sep 17 00:00:00 2001 From: zhongchao Date: Fri, 7 Jan 2022 20:37:26 +0800 Subject: [PATCH] add logcatch debug view --- .../logcatch/MogoLogCatchManager.kt | 6 +- .../function/hmi/ui/logcatch/AbsLogView.kt | 496 ++++++++++++++++++ .../hmi/ui/logcatch/AbsRecyclerAdapter.java | 148 ++++++ .../hmi/ui/logcatch/AbsViewBinder.java | 55 ++ .../core/function/hmi/ui/logcatch/ILogView.kt | 82 +++ .../hmi/ui/logcatch/ILogViewListener.kt | 6 + .../hmi/ui/logcatch/LastLogViewPosInfo.kt | 30 ++ .../hmi/ui/logcatch/LogFrameLayout.kt | 26 + .../function/hmi/ui/logcatch/LogInfoView.kt | 287 ++++++++++ .../hmi/ui/logcatch/LogItemAdapter.java | 260 +++++++++ .../function/hmi/ui/logcatch/LogLine.java | 202 +++++++ .../function/hmi/ui/logcatch/LogTitleBar.java | 114 ++++ .../hmi/ui/logcatch/LogViewLayoutParams.java | 49 ++ .../function/hmi/ui/logcatch/TouchProxy.java | 32 ++ .../hmi/ui/setting/DebugSettingView.kt | 54 +- .../function/hmi/ui/utils/SearchCriteria.kt | 120 +++++ .../function/hmi/ui/utils/TagColorUtil.kt | 68 +++ .../main/res/drawable-xxhdpi/close_icon.png | Bin 0 -> 3399 bytes .../res/drawable-xxhdpi/shadow_bottom.png | Bin 0 -> 157 bytes .../src/main/res/drawable/input_cursor.xml | 5 + .../res/drawable/log_filter_background.xml | 11 + .../res/drawable/radio_button_background.xml | 5 + .../drawable/radio_button_background_left.xml | 5 + .../radio_button_background_middle.xml | 5 + .../radio_button_background_right.xml | 5 + .../radio_button_checked_background.xml | 13 + .../radio_button_checked_background_left.xml | 15 + ...radio_button_checked_background_middle.xml | 14 + .../radio_button_checked_background_right.xml | 15 + .../radio_button_normal_background.xml | 13 + .../radio_button_normal_background_left.xml | 15 + .../radio_button_normal_background_middle.xml | 14 + .../radio_button_normal_background_right.xml | 15 + .../src/main/res/layout/item_log.xml | 74 +++ .../src/main/res/layout/log_title_bar.xml | 26 + .../src/main/res/layout/log_view.xml | 164 ++++++ .../main/res/layout/view_debug_setting.xml | 10 + .../src/main/res/values/attr.xml | 6 + .../src/main/res/values/color.xml | 23 + .../src/main/res/values/strings.xml | 17 + .../src/main/res/values/styles.xml | 76 +++ .../api/devatools/IMoGoDevaToolsListener.kt | 9 + .../CallerDevaToolsListenerManager.kt | 7 + gradle.properties | 2 +- 44 files changed, 2594 insertions(+), 5 deletions(-) create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/AbsLogView.kt create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/AbsRecyclerAdapter.java create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/AbsViewBinder.java create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/ILogView.kt create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/ILogViewListener.kt create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/LastLogViewPosInfo.kt create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/LogFrameLayout.kt create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/LogInfoView.kt create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/LogItemAdapter.java create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/LogLine.java create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/LogTitleBar.java create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/LogViewLayoutParams.java create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/TouchProxy.java create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/utils/SearchCriteria.kt create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/utils/TagColorUtil.kt create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/drawable-xxhdpi/close_icon.png create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/drawable-xxhdpi/shadow_bottom.png create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/drawable/input_cursor.xml create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/drawable/log_filter_background.xml create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/drawable/radio_button_background.xml create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/drawable/radio_button_background_left.xml create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/drawable/radio_button_background_middle.xml create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/drawable/radio_button_background_right.xml create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/drawable/radio_button_checked_background.xml create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/drawable/radio_button_checked_background_left.xml create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/drawable/radio_button_checked_background_middle.xml create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/drawable/radio_button_checked_background_right.xml create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/drawable/radio_button_normal_background.xml create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/drawable/radio_button_normal_background_left.xml create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/drawable/radio_button_normal_background_middle.xml create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/drawable/radio_button_normal_background_right.xml create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/layout/item_log.xml create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/layout/log_title_bar.xml create mode 100644 core/function-impl/mogo-core-function-hmi/src/main/res/layout/log_view.xml diff --git a/core/function-impl/mogo-core-function-devatools/src/main/java/com/zhjt/mogo_core_function_devatools/logcatch/MogoLogCatchManager.kt b/core/function-impl/mogo-core-function-devatools/src/main/java/com/zhjt/mogo_core_function_devatools/logcatch/MogoLogCatchManager.kt index 50acd3b2d0..36610d631f 100644 --- a/core/function-impl/mogo-core-function-devatools/src/main/java/com/zhjt/mogo_core_function_devatools/logcatch/MogoLogCatchManager.kt +++ b/core/function-impl/mogo-core-function-devatools/src/main/java/com/zhjt/mogo_core_function_devatools/logcatch/MogoLogCatchManager.kt @@ -109,8 +109,12 @@ object MogoLogCatchManager : IMogoOnMessageListener, Handl logInfoManager = LogInfoManagerFactory.createPushLogInfoManager( mContext, MoGoAiCloudClientConfig.getInstance().sn + File.separator + TimeUtils.formatYMD(System.currentTimeMillis()), - content, this) + content, this + ) logInfoManager?.start() + logInfoManager?.registerLogOutListener { lineLog -> + CallerDevaToolsListenerManager.invokeDevaToolsLogCatchLines(lineLog) + } } private fun stopCatchLog(content: RemoteLogPushContent) { diff --git a/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/AbsLogView.kt b/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/AbsLogView.kt new file mode 100644 index 0000000000..8af3873f7b --- /dev/null +++ b/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/AbsLogView.kt @@ -0,0 +1,496 @@ +package com.mogo.eagle.core.function.hmi.ui.logcatch + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.res.Resources +import android.graphics.PixelFormat +import android.util.Log +import android.view.* +import android.widget.FrameLayout +import androidx.annotation.IdRes +import androidx.annotation.StringRes +import androidx.core.view.GravityCompat +import com.mogo.commons.context.ContextHolderUtil +import com.mogo.eagle.core.utilcode.util.ScreenUtils +import com.mogo.eagle.core.utilcode.util.Utils + +abstract class AbsLogView : ILogView, TouchProxy.OnTouchEventListener { + + class ViewArgs { + var edgePinned = false + } + + private val mInnerReceiver = InnerReceiver() + + @JvmField + protected var mWindowManager = + ContextHolderUtil.getContext().getSystemService(Context.WINDOW_SERVICE) as WindowManager + + /** + * 创建FrameLayout#LayoutParams 系统悬浮窗调用 + */ + protected var systemLayoutParams: WindowManager.LayoutParams? = null + + /** + * 整个悬浮窗的View + */ + private var mRootView: FrameLayout? = null + + /** + * rootView的直接子View 一般是用户的xml布局 被添加到mRootView中 + */ + private var mChildView: View? = null + + val logView: View? + get() = mRootView + + /** + * 手势代理 + */ + @JvmField + var mTouchProxy = TouchProxy(this) + + private val viewProps = ViewArgs() + + /** + * 控件在布局边界发生大小变化被裁剪的原因 + */ + val parentView: LogFrameLayout? + get() = if (mRootView != null) { + mRootView!!.parent as LogFrameLayout + } else null + + fun show(context: Context) { + if (isShow) { + Log.d("EmArrow", "isShow : $isShow") + return + } + performCreate(context) + createView() + onResume() + } + + fun dismiss() { + removeView() + performDestroy() + } + + /** + * 执行floatPage create + * + * @param context 上下文环境 + */ + @SuppressLint("ClickableViewAccessibility") + private fun performCreate(context: Context) { + try { + //调用onCreate方法 + onCreate(context) + + //系统悬浮窗的返回按键监听 + mRootView = object : LogFrameLayout(context, LogFrameLayoutFlag_CHILD) { + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (event.action == KeyEvent.ACTION_UP && shouldDealBackKey()) { + //监听返回键 + if (event.keyCode == KeyEvent.KEYCODE_BACK || event.keyCode == KeyEvent.KEYCODE_HOME) { + return onBackPressed() + } + } + return super.dispatchKeyEvent(event) + } + } + //添加根布局的layout回调 + addViewTreeObserverListener() + //调用onCreateView抽象方法 + mChildView = onCreateView(context, mRootView) + //将子View添加到rootView中 + mRootView?.addView(mChildView) + //设置根布局的手势拦截 + mRootView?.setOnTouchListener { v, event -> mTouchProxy.onTouchEvent(v, event) } + //调用onViewCreated回调 + onViewCreated(mRootView) + + mLogViewLayoutParams = LogViewLayoutParams() + //分别创建对应的LayoutParams + systemLayoutParams = WindowManager.LayoutParams() + //shouldDealBackKey : false 不自己收返回事件处理 + if (shouldDealBackKey()) { + //自己处理返回按键 + systemLayoutParams?.flags = + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + mLogViewLayoutParams.flags = + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or LogViewLayoutParams.FLAG_LAYOUT_NO_LIMITS + } else { + //设置WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE会导致RootView监听不到返回按键的监听失效 系统处理返回按键 + systemLayoutParams?.flags = + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + mLogViewLayoutParams.flags = + LogViewLayoutParams.FLAG_NOT_FOCUSABLE or LogViewLayoutParams.FLAG_LAYOUT_NO_LIMITS + } + systemLayoutParams?.apply { + format = PixelFormat.TRANSPARENT + gravity = GravityCompat.START or Gravity.TOP + } + mLogViewLayoutParams.gravity = GravityCompat.START or Gravity.TOP + //动态注册关闭系统弹窗的广播 + val intentFilter = IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS) + context.registerReceiver(mInnerReceiver, intentFilter) + initViewLayoutParams(mLogViewLayoutParams) + onSystemLayoutParamsCreated() + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun createView() { + mWindowManager.addView(logView, systemLayoutParams) + } + + override fun onResume() { + mRootView?.requestLayout() + } + + private fun removeView() { + mWindowManager.removeView(logView) + } + + private fun performDestroy() { + context?.unregisterReceiver(mInnerReceiver) + //移除布局监听 + removeViewTreeObserverListener() + mRootView = null + onDestroy() + } + + /** + * 用来保存rootView的LayoutParams + */ + private lateinit var mLogViewLayoutParams: LogViewLayoutParams + + /** + * 上一次LogView的位置信息 + */ + private val mLastLogViewPosInfo: LastLogViewPosInfo = LastLogViewPosInfo() + + /** + * 根布局的实际宽 + */ + private var mLogViewWidth = 0 + + /** + * 根布局的实际高 + */ + private var mLogViewHeight = 0 + + private var mViewTreeObserver: ViewTreeObserver? = null + + private val mOnGlobalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener = + ViewTreeObserver.OnGlobalLayoutListener { + //每次布局发生变动的时候重新赋值 + mRootView?.let { + mLogViewWidth = it.measuredWidth + mLogViewHeight = it.measuredHeight + mLastLogViewPosInfo.logViewWidth = mLogViewWidth + mLastLogViewPosInfo.logViewHeight = mLogViewHeight + } + } + + private fun addViewTreeObserverListener() { + if (mViewTreeObserver == null && mRootView != null) { + mViewTreeObserver = mRootView!!.viewTreeObserver + mViewTreeObserver?.addOnGlobalLayoutListener(mOnGlobalLayoutListener) + } + } + + private fun removeViewTreeObserverListener() { + mViewTreeObserver?.let { + if (it.isAlive) { + it.removeOnGlobalLayoutListener(mOnGlobalLayoutListener) + } + } + } + + override fun onDestroy() { + } + + /** + * 确定系统浮标的初始位置 + * LayoutParams创建完以后调用 + * 调用时建议放在实现下方 + */ + private fun onSystemLayoutParamsCreated() { + //如果有上一个页面的位置记录 这更新位置 + systemLayoutParams?.flags = mLogViewLayoutParams.flags + systemLayoutParams?.gravity = mLogViewLayoutParams.gravity + systemLayoutParams?.width = mLogViewLayoutParams.width + systemLayoutParams?.height = mLogViewLayoutParams.height + systemLayoutParams?.x = mLogViewLayoutParams.x + systemLayoutParams?.y = mLogViewLayoutParams.y + } + + /** + * 默认实现为true + * + * @return + */ + override fun canDrag(): Boolean { + return true + } + + /** + * 搭配shouldDealBackKey使用 自定义处理完以后需要返回true + * 默认模式的onBackPressed 拦截在NormViewManager#getRootContentView中被处理 + * 系统模式下的onBackPressed 在当前类的performCreate 初始化View时被处理 + * 返回false 表示交由系统处理 + * 返回 true 表示当前的返回事件已由自己处理 并拦截了改返回事件 + */ + override fun onBackPressed(): Boolean { + return false + } + + /** + * 默认不自己处理返回按键 + * + * @return + */ + override fun shouldDealBackKey(): Boolean { + return false + } + + override fun onEnterBackground() { + mRootView?.let { + it.visibility = View.GONE + } + } + + override fun onEnterForeground() { + mRootView?.let { + it.visibility = View.VISIBLE + } + } + + override fun onMove(x: Int, y: Int, dx: Int, dy: Int) { + if (!canDrag()) { + return + } + systemLayoutParams?.apply { + this.x += dx + this.y += dy + } + //限制布局边界 + resetBorderline(systemLayoutParams) + mWindowManager.updateViewLayout(mRootView, systemLayoutParams) + } + + /** + * 限制边界 调用的时候必须保证是在控件能获取到宽高德前提下 + */ + private fun resetBorderline( + windowLayoutParams: WindowManager.LayoutParams? + ) { + //如果是系统模式或者手动关闭动态限制边界 + if (!restrictBorderline()) { + return + } + + if (windowLayoutParams != null) { + + // 均是横向计算 + if (windowLayoutParams.y >= screenShortSideLength - mLogViewHeight) { + windowLayoutParams.y = screenShortSideLength - mLogViewHeight + } + // 均是横向计算 + if (windowLayoutParams.x >= screenLongSideLength - mLogViewWidth) { + windowLayoutParams.x = screenLongSideLength - mLogViewWidth + } + + //系统模式 + if (windowLayoutParams.y <= 0) { + windowLayoutParams.y = 0 + } + + if (windowLayoutParams.x <= 0) { + windowLayoutParams.x = 0 + } + } + } + + /** + * 手指弹起时保存当前浮标位置 + * + * @param x + * @param y + */ + override fun onUp(x: Int, y: Int) { + if (!canDrag()) { + return + } + if (!viewProps.edgePinned) { + endMoveAndRecord() + return + } + animatedMoveToEdge() + } + + private fun endMoveAndRecord() { + systemLayoutParams?.let { + mLastLogViewPosInfo.logViewWidth = it.x + mLastLogViewPosInfo.logViewHeight = it.y + } + } + + private fun animatedMoveToEdge() { + val viewSize = mRootView?.width ?: return + systemLayoutParams?.also { layoutAttrs -> + makeAnimator(layoutAttrs.x, viewSize, ViewGroup.LayoutParams.MATCH_PARENT) { + addUpdateListener { v -> + layoutAttrs.x = v.animatedValue as Int + mWindowManager.updateViewLayout(mRootView, layoutAttrs) + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + endMoveAndRecord() + } + }) + } + } + } + + private inline fun makeAnimator( + from: Int, + size: Int, + containerSize: Int, + setup: ValueAnimator.() -> Unit + ) { + if (size <= 0 || containerSize <= 0) return + ValueAnimator.ofInt( + from, + if (from <= (containerSize - size) / 2) 0 else (containerSize - size) + ) + .apply { + duration = 150L + setup() + } + .start() + } + + /** + * 手指按下时的操作 + * + * @param x + * @param y + */ + override fun onDown(x: Int, y: Int) { + if (!canDrag()) { + return + } + } + + /** + * home键被点击 只有系统悬浮窗控件才会被调用 + */ + open fun onHomeKeyPress() {} + + /** + * 菜单键被点击 只有系统悬浮窗控件才会被调用 + */ + open fun onRecentAppKeyPress() {} + + override fun onPause() {} + + /** + * 系统悬浮窗需要调用 + * + * @return + */ + val context: Context? + get() = if (mRootView != null) { + mRootView!!.context + } else { + null + } + + val resources: Resources? + get() = if (context == null) { + null + } else context!!.resources + + fun getString(@StringRes resId: Int): String? { + return if (context == null) { + null + } else context!!.getString(resId) + } + + private val isShow: Boolean + get() = mRootView?.isShown ?: false + + protected fun findViewById(@IdRes id: Int): T? { + if (mRootView == null) { + return null + } + return mRootView?.findViewById(id) + } + + /** + * 是否限制布局边界 + * + * @return + */ + open fun restrictBorderline(): Boolean { + return true + } + + /** + * 获取屏幕短边的长度(横屏) 不包含statusBar + * + * @return + */ + private val screenShortSideLength: Int + get() = ScreenUtils.getAppScreenHeight() + + /** + * 获取屏幕长边的长度(横屏) 不包含statusBar + * + * @return + */ + private val screenLongSideLength: Int + get() = ScreenUtils.getAppScreenWidth() + + /** + * 强制刷新当前view + */ + open fun immInvalidate() { + mRootView?.requestLayout() + } + + /** + * 广播接收器 系统悬浮窗需要调用 + */ + private inner class InnerReceiver : BroadcastReceiver() { + + private val SYSTEM_DIALOG_REASON_KEY = "reason" + private val SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps" + private val SYSTEM_DIALOG_REASON_HOME_KEY = "homekey" + + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action + if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS == action) { + val reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY) + if (reason != null) { + if (reason == SYSTEM_DIALOG_REASON_HOME_KEY) { + //点击home键 + onHomeKeyPress() + } else if (reason == SYSTEM_DIALOG_REASON_RECENT_APPS) { + //点击menu按钮 + onRecentAppKeyPress() + } + } + } + } + } +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/AbsRecyclerAdapter.java b/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/AbsRecyclerAdapter.java new file mode 100644 index 0000000000..a3b5ebc955 --- /dev/null +++ b/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/AbsRecyclerAdapter.java @@ -0,0 +1,148 @@ +package com.mogo.eagle.core.function.hmi.ui.logcatch; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * 内置一个List的通用、简化的适用于RecyclerView的Adapter。 + */ +public abstract class AbsRecyclerAdapter extends RecyclerView.Adapter { + protected List mList; + private LayoutInflater mInflater; + protected Context mContext; + + public AbsRecyclerAdapter(Context context) { + if (context == null) { + return; + } + mContext = context; + mList = new ArrayList<>(); + mInflater = LayoutInflater.from(context); + } + + @NonNull + @Override + public final T onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = createView(mInflater, parent, viewType); + return createViewHolder(view, viewType); + } + + protected abstract T createViewHolder(View view, int viewType); + + /** + * 如果是通过LayoutInflater创建的View,不要绑定到父View,RecyclerView会负责添加。 + */ + protected abstract View createView(LayoutInflater inflater, ViewGroup parent, int viewType); + + @Override + public final void onBindViewHolder(T holder, int position) { + V data = mList.get(position); + holder.setData(data); + holder.bind(data, position); + } + + @Override + public int getItemCount() { + return mList.size(); + } + + /** + * 列表末尾追加一个元素 + */ + @SuppressLint("NotifyDataSetChanged") + public void append(V item) { + if (item == null) { + return; + } + mList.add(item); + notifyDataSetChanged(); + } + + /** + * 在特定位置增加一个元素 + */ + public void append(V item, int position) { + if (item == null) { + return; + } + if (position < 0) { + position = 0; + } else if (position > mList.size()) { + position = mList.size(); + } + mList.add(position, item); + notifyItemChanged(position, item); + } + + /** + * 追加一个集合 + */ + @SuppressLint("NotifyDataSetChanged") + public final void append(Collection items) { + if (items == null || items.size() == 0) { + return; + } + mList.addAll(items); + notifyDataSetChanged(); + } + + /** + * 清空集合 + */ + @SuppressLint("NotifyDataSetChanged") + public final void clear() { + if (mList.isEmpty()) { + return; + } + mList.clear(); + notifyDataSetChanged(); + } + + /** + * 删除一个元素 + */ + @SuppressLint("NotifyDataSetChanged") + public final void remove(V item) { + if (item == null) { + return; + } + if (mList.contains(item)) { + mList.remove(item); + notifyDataSetChanged(); + } + } + + /** + * 删除一个元素 + */ + public final void remove(int index) { + if (index < mList.size()) { + mList.remove(index); + notifyItemRemoved(index); + } + } + + /** + * 删除一个集合 + */ + @SuppressLint("NotifyDataSetChanged") + public final void remove(Collection items) { + if (items == null || items.size() == 0) { + return; + } + if (mList.removeAll(items)) { + notifyDataSetChanged(); + } + } + +} diff --git a/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/AbsViewBinder.java b/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/AbsViewBinder.java new file mode 100644 index 0000000000..c3c08f142e --- /dev/null +++ b/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/AbsViewBinder.java @@ -0,0 +1,55 @@ +package com.mogo.eagle.core.function.hmi.ui.logcatch; + +import android.content.Context; +import android.view.View; + +import androidx.annotation.IdRes; +import androidx.recyclerview.widget.RecyclerView; + +/** + * 简单封装的适用于RecyclerView的ViewHolder + */ +public abstract class AbsViewBinder extends RecyclerView.ViewHolder { + private T data; + + private View mView; + + public AbsViewBinder(final View view) { + super(view); + mView = view; + getViews(); + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onViewClick(view, data); + } + }); + } + + protected final View getView() { + return mView; + } + + protected abstract void getViews(); + + public final V getView(@IdRes int id) { + return (V) mView.findViewById(id); + } + + public abstract void bind(T t); + + public void bind(T t, int position) { + bind(t); + } + + protected void onViewClick(View view, T data) { + } + + protected final void setData(T data) { + this.data = data; + } + + protected final Context getContext() { + return mView.getContext(); + } +} diff --git a/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/ILogView.kt b/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/ILogView.kt new file mode 100644 index 0000000000..4c49bd8610 --- /dev/null +++ b/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/ILogView.kt @@ -0,0 +1,82 @@ +package com.mogo.eagle.core.function.hmi.ui.logcatch + +import android.content.Context +import android.view.View +import android.widget.FrameLayout + +interface ILogView { + /** + * view 创建时调用 做一些变量的初始化 当还不能进行View的操作 + * + * @param context + */ + fun onCreate(context: Context?) + + /** + * 传入rootView 用于创建控件 + * + * @param context + * @param rootView + * @return 返回创建的childView + */ + fun onCreateView(context: Context?, rootView: FrameLayout?): View? + + /** + * 将xml中的控件添加到rootView以后调用,在当前方法中可以进行view的一些操作 + * + * @param rootView + */ + fun onViewCreated(rootView: FrameLayout?) + + /** + * 当前的View添加到根布局里时调用 + */ + fun onResume() + + /** + * 当前activity onPause时调用 + */ + fun onPause() + + /** + * 确定系统悬浮窗浮标的初始位置 + * LayoutParams创建完以后调用 + * + * @param params + */ + fun initViewLayoutParams(params: LogViewLayoutParams?) + + /** + * app进入后台时调用 内置View 不需要实现 + */ + fun onEnterBackground() + + /** + * app回到前台时调用 内置view 不需要实现 + */ + fun onEnterForeground() + + /** + * 浮标控件是否可以拖动 + * + * @return + */ + fun canDrag(): Boolean + + /** + * 是否需要自己处理返回键 + * + * @return + */ + fun shouldDealBackKey(): Boolean + + /** + * shouldDealBackKey == true 时调用 + */ + fun onBackPressed(): Boolean + + /** + * 悬浮窗主动销毁时调用 不能在当前生命周期回调函数中调用 detach自己 否则会出现死循环 + */ + fun onDestroy() +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/ILogViewListener.kt b/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/ILogViewListener.kt new file mode 100644 index 0000000000..16c65363b0 --- /dev/null +++ b/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/ILogViewListener.kt @@ -0,0 +1,6 @@ +package com.mogo.eagle.core.function.hmi.ui.logcatch + +interface ILogViewListener { + fun onAttach() + fun onDetach() +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/LastLogViewPosInfo.kt b/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/LastLogViewPosInfo.kt new file mode 100644 index 0000000000..3a6780a289 --- /dev/null +++ b/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/LastLogViewPosInfo.kt @@ -0,0 +1,30 @@ +package com.mogo.eagle.core.function.hmi.ui.logcatch + +import com.mogo.eagle.core.utilcode.util.ScreenUtils + +/** + * 保存上一次LogView的位置信息 + */ +class LastLogViewPosInfo { + var logViewWidth = 0 + var logViewHeight = 0 + var leftMarginPercent = 0f + private set + var topMarginPercent = 0f + private set + + fun setLeftMargin(leftMargin: Int) { + leftMarginPercent = leftMargin.toFloat() / ScreenUtils.getAppScreenWidth().toFloat() + } + + fun setTopMargin(topMargin: Int) { + topMarginPercent = topMargin.toFloat() / ScreenUtils.getAppScreenHeight().toFloat() + } + + override fun toString(): String { + return "LastLogViewPosInfo{" + + ", leftMarginPercent=" + leftMarginPercent + + ", topMarginPercent=" + topMarginPercent + + '}' + } +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/LogFrameLayout.kt b/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/LogFrameLayout.kt new file mode 100644 index 0000000000..352ad9b2ea --- /dev/null +++ b/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/LogFrameLayout.kt @@ -0,0 +1,26 @@ +package com.mogo.eagle.core.function.hmi.ui.logcatch + +import android.content.Context +import android.util.AttributeSet +import android.widget.FrameLayout + +open class LogFrameLayout : FrameLayout { + private var mFlag = LogFrameLayoutFlag_ROOT + + constructor(context: Context, flag: Int) : super(context) { + mFlag = flag + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {} + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { + } + + companion object { + const val LogFrameLayoutFlag_ROOT = 100 + const val LogFrameLayoutFlag_CHILD = 200 + } +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/LogInfoView.kt b/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/LogInfoView.kt new file mode 100644 index 0000000000..6a8e8bf27c --- /dev/null +++ b/core/function-impl/mogo-core-function-hmi/src/main/java/com/mogo/eagle/core/function/hmi/ui/logcatch/LogInfoView.kt @@ -0,0 +1,287 @@ +package com.mogo.eagle.core.function.hmi.ui.logcatch + +import android.annotation.SuppressLint +import android.content.Context +import android.text.Editable +import android.text.TextWatcher +import android.util.Log +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.WindowManager +import android.widget.* +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.mogo.eagle.core.function.hmi.R +import java.util.* + +class LogInfoView : AbsLogView() { + + companion object { + const val MAX_LOG_LINE_NUM = 10000 + const val UPDATE_CHECK_INTERVAL = 200 + } + + private var logLines = LinkedList() + private var counter = 0 + private var mAutoScrollToBottom = true + private var mIsLoaded = false + + private var mLogRv: RecyclerView? = null + private var mLogItemAdapter: LogItemAdapter? = null + private var mLogFilter: EditText? = null + private var mRadioGroup: RadioGroup? = null + private var mLinearLayout: LinearLayout? = null + + /** + * 单行的log + */ + private var mLogHint: TextView? = null + private var mLogRvWrap: RelativeLayout? = null + + private var logViewListener: ILogViewListener? = null + + override fun onCreate(context: Context?) {} + override fun onCreateView(context: Context?, rootView: FrameLayout?): View? { + return LayoutInflater.from(context).inflate(R.layout.log_view, rootView, false) + } + + override fun onViewCreated(rootView: FrameLayout?) { + initView() + } + + private fun initView() { + mLogHint = findViewById(R.id.log_hint) + mLogRvWrap = findViewById(R.id.log_page) + mLogRv = findViewById(R.id.log_list) + mLogRv!!.layoutManager = LinearLayoutManager(context) + mLogItemAdapter = LogItemAdapter(context!!) + mLogRv!!.adapter = mLogItemAdapter + mLogFilter = findViewById(R.id.log_filter) + mLogFilter!!.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable) { + mLogItemAdapter!!.filter.filter(s) + } + }) + val mTitleBar = findViewById(R.id.title_bar) + mTitleBar!!.setListener(object : LogTitleBar.OnTitleBarClickListener { + override fun onRightClick() { + if (logViewListener != null) { + logViewListener!!.onDetach() + } + } + + override fun onLeftClick() { + minimize() + } + }) + mLogHint!!.setOnClickListener { v: View? -> maximize() } + mRadioGroup = findViewById(R.id.radio_group) + mRadioGroup!!.setOnCheckedChangeListener { group: RadioGroup?, checkedId: Int -> + when (checkedId) { + R.id.verbose -> { + mLogItemAdapter!!.logLevelLimit = Log.VERBOSE + } + R.id.debug -> { + mLogItemAdapter!!.logLevelLimit = Log.DEBUG + } + R.id.info -> { + mLogItemAdapter!!.logLevelLimit = Log.INFO + } + R.id.warn -> { + mLogItemAdapter!!.logLevelLimit = Log.WARN + } + R.id.error -> { + mLogItemAdapter!!.logLevelLimit = Log.ERROR + } + } + mLogItemAdapter!!.filter.filter(mLogFilter!!.text) + } + mLogRv!!.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + } + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + val layoutManager = recyclerView.layoutManager as LinearLayoutManager? + // if the bottom of the list isn't visible anymore, then stop autoscrolling + mAutoScrollToBottom = + layoutManager!!.findLastCompletelyVisibleItemPosition() == recyclerView.adapter!! + .itemCount - 1 + } + }) + mRadioGroup!!.check(R.id.verbose) + val mBtnTop = findViewById