Android 10.0 滑动解锁流程(上)

简介: Android 10.0 滑动解锁流程(上)

学习笔记:

滑动解锁相对于来说逻辑还是简单的,说白了就是对事件的处理,然后做一些事。

这里主要从锁屏的界面Layout结构、touchEvent事件分发、解锁动作逻辑几个方面进行源码的分析。

锁屏的界面Layout结构分析

StatusbarWindowView

整个锁屏界面的顶级 View 就是 StatusbarWindowView;

StatusBar#createAndAddWindows()

// StatusBar.java
    public void createAndAddWindows(@Nullable RegisterStatusBarResult result) {
        makeStatusBarView(result);
        mNotificationShadeWindowController.attach();
        // 添加视图
        mStatusBarWindowController.attach();
    }

StatusBarWindowController#attach()

// StatusBarWindowController.java
    public void attach() {
        // Now that the status bar window encompasses the sliding panel and its
        // translucent backdrop, the entire thing is made TRANSLUCENT and is
        // hardware-accelerated.
        mLp = new WindowManager.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                mBarHeight,
                WindowManager.LayoutParams.TYPE_STATUS_BAR,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                        | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
                        | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
                PixelFormat.TRANSLUCENT);
        mLp.privateFlags |= PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC;
        mLp.token = new Binder();
        mLp.gravity = Gravity.TOP;
        mLp.setFitInsetsTypes(0 /* types */);
        mLp.setTitle("StatusBar");
        mLp.packageName = mContext.getPackageName();
        mLp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
        mWindowManager.addView(mStatusBarView, mLp);
        mLpChanged.copyFrom(mLp);
    }

StatusBarWindow 是在 StatusBar 的 createAndAddWindows() 流程中调用StatusBarWindowController.attach() 添加到窗口上的, type为WindowManager.LayoutParams.TYPE_STATUS_BAR

Layout结构

锁屏界面的Layout结构可以简单概括为以下结构:

  • mStatusBarWindow--> R.layout.super_status_bar
  • notification_panel--> R.layout.status_bar_expanded
  • keyguardBouncer-->R.layout.keyguard_bouncer

mStatusBarWindow-->notification_panel-->notification_container_parent-->keyguard_header(锁屏状态栏)
                |                    |
                |                    -->keyguard_bottom_area (lock_icon和充电状态等)
                |                    |
                |                    -->keyguard_status_view (锁屏时钟日期)
                |                    |
                |                    -->keyguard_up_slide (箭头提示动画)
                |
                -->keyguardBouncer(安全锁界面)

在这里 keyguardBouncer 加载就不分析了,前面有说过的,见锁屏加载分析


touchEvent事件分发


我们这里分析上滑解锁过程中的touchEvent事件分发

android中的事件分发概念:事件序列。


事件序列


在Android系统中,一个单独的事件基本上是没什么作用的,只有一个事件序列,才有意义。一个事件序列正常情况下,定义为 DOWN、MOVE(0或者多个)、UP/CANCEL。事件序列以DOWN事件开始,中间会有0或者多个MOVE事件,最后以UP事件或者CANCEL事件结束。

DOWN事件作为序列的开始,有一个很重要的职责,就是寻找事件序列的接受者,怎么理解呢?framework 在DOWN事件的传递过程中,需要根据View事件处理方法(onTouchEvent)的返回值来确定事件序列的接受者。如果一个View的onTouchEvent事件,在处理DOWN事件的时候返回true,说明它愿意接受并处理该事件序列。


上滑解锁


当用户移动手指时,产生touch down事件,最外层view StatusBarWindowView会执行onInterceptTouchEvent,看是否需要拦截touch事件。再一级级往子View传递,都没有被拦截,之后执行OnTouchEvent从子View开始一级级往父View传递,到PanelView这里当手指移动的距离达到一定的阈值会调用onTrackingStarted从而设置mTracking的值为true,onTouchEvent返回true,接收此touch move事件,之后的touch事件直接传到此View。

在用户滑动过程会调用setExpandedHeightInternal,进而调用NotificationPanelView的onHeightUpdated进行锁屏上的时间和通知View根据手指的移动距离进行缩小、变透明处理。

当用户抬起手指时,产生touch up事件,PanelView接收到这个事件后会调用endMotionEvent,如果手指从down到up之间移动的距离达到一定阈值会调用onTrackingStopped。

1.硬件发出指令:按下,移动,抬起

2.input接收

3.代码执行相应操作:ACTION_DOWN,ACTION_MOVE,ACTION_UP

PanelView#onInterceptTouchEvent()

// PanelView.java
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        return mTouchHandler.onInterceptTouchEvent(event);
    }

PanelViewController

// PanelViewController.java
   public class TouchHandler implements View.OnTouchListener {
        public boolean onInterceptTouchEvent(MotionEvent event) {
            if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled || (mMotionAborted
                    && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
                return false;
            }
            /*
             * If the user drags anywhere inside the panel we intercept it if the movement is
             * upwards. This allows closing the shade from anywhere inside the panel.
             *
             * We only do this if the current content is scrolled to the bottom,
             * i.e canCollapsePanelOnTouch() is true and therefore there is no conflicting scrolling
             * gesture
             * possible.
             */
            int pointerIndex = event.findPointerIndex(mTrackingPointer);
            if (pointerIndex < 0) {
                pointerIndex = 0;
                mTrackingPointer = event.getPointerId(pointerIndex);
            }
            final float x = event.getX(pointerIndex);
            final float y = event.getY(pointerIndex);
            boolean canCollapsePanel = canCollapsePanelOnTouch();
            switch (event.getActionMasked()) {
                case MotionEvent.ACTION_DOWN:
                    mStatusBar.userActivity();
                    mAnimatingOnDown = mHeightAnimator != null;
                    mMinExpandHeight = 0.0f;
                    mDownTime = SystemClock.uptimeMillis();
                    if (mAnimatingOnDown && mClosing && !mHintAnimationRunning
                            || mPeekAnimator != null) {
                        cancelHeightAnimator();
                        cancelPeek();
                        mTouchSlopExceeded = true;
                        return true;
                    }
                    mInitialTouchY = y;
                    mInitialTouchX = x;
                    mTouchStartedInEmptyArea = !isInContentBounds(x, y);
                    mTouchSlopExceeded = mTouchSlopExceededBeforeDown;
                    mJustPeeked = false;
                    mMotionAborted = false;
                    mPanelClosedOnDown = isFullyCollapsed();
                    mCollapsedAndHeadsUpOnDown = false;
                    mHasLayoutedSinceDown = false;
                    mUpdateFlingOnLayout = false;
                    mTouchAboveFalsingThreshold = false;
                    addMovement(event);
                    break;
                case MotionEvent.ACTION_POINTER_UP:
                    final int upPointer = event.getPointerId(event.getActionIndex());
                    if (mTrackingPointer == upPointer) {
                        // gesture is ongoing, find a new pointer to track
                        final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
                        mTrackingPointer = event.getPointerId(newIndex);
                        mInitialTouchX = event.getX(newIndex);
                        mInitialTouchY = event.getY(newIndex);
                    }
                    break;
                case MotionEvent.ACTION_POINTER_DOWN:
                    if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
                        mMotionAborted = true;
                        mVelocityTracker.clear();
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    final float h = y - mInitialTouchY;
                    addMovement(event);
                    if (canCollapsePanel || mTouchStartedInEmptyArea || mAnimatingOnDown) {
                        float hAbs = Math.abs(h);
                        float touchSlop = getTouchSlop(event);
                        if ((h < -touchSlop || (mAnimatingOnDown && hAbs > touchSlop))
                                && hAbs > Math.abs(x - mInitialTouchX)) {
                            cancelHeightAnimator();
                            startExpandMotion(x, y, true /* startTracking */, mExpandedHeight);
                            return true;
                        }
                    }
                    break;
                case MotionEvent.ACTION_CANCEL:
                case MotionEvent.ACTION_UP:
                    mVelocityTracker.clear();
                    break;
            }
            return false;
        }
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if (mInstantExpanding || (mTouchDisabled
                    && event.getActionMasked() != MotionEvent.ACTION_CANCEL) || (mMotionAborted
                    && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
                return false;
            }
            // If dragging should not expand the notifications shade, then return false.
            if (!mNotificationsDragEnabled) {
                if (mTracking) {
                    // Turn off tracking if it's on or the shade can get stuck in the down position.
                    onTrackingStopped(true /* expand */);
                }
                return false;
            }
            // On expanding, single mouse click expands the panel instead of dragging.
            if (isFullyCollapsed() && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
                if (event.getAction() == MotionEvent.ACTION_UP) {
                    expand(true);
                }
                return true;
            }
            /*
             * We capture touch events here and update the expand height here in case according to
             * the users fingers. This also handles multi-touch.
             *
             * If the user just clicks shortly, we show a quick peek of the shade.
             *
             * Flinging is also enabled in order to open or close the shade.
             */
            int pointerIndex = event.findPointerIndex(mTrackingPointer);
            if (pointerIndex < 0) {
                pointerIndex = 0;
                mTrackingPointer = event.getPointerId(pointerIndex);
            }
            final float x = event.getX(pointerIndex);
            final float y = event.getY(pointerIndex);
            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
                mGestureWaitForTouchSlop = shouldGestureWaitForTouchSlop();
                mIgnoreXTouchSlop = isFullyCollapsed() || shouldGestureIgnoreXTouchSlop(x, y);
            }
            switch (event.getActionMasked()) {
                case MotionEvent.ACTION_DOWN:
                    startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
                    mJustPeeked = false;
                    mMinExpandHeight = 0.0f;
                    mPanelClosedOnDown = isFullyCollapsed();
                    mHasLayoutedSinceDown = false;
                    mUpdateFlingOnLayout = false;
                    mMotionAborted = false;
                    mPeekTouching = mPanelClosedOnDown;
                    mDownTime = SystemClock.uptimeMillis();
                    mTouchAboveFalsingThreshold = false;
                    mCollapsedAndHeadsUpOnDown =
                            isFullyCollapsed() && mHeadsUpManager.hasPinnedHeadsUp();
                    addMovement(event);
                    if (!mGestureWaitForTouchSlop || (mHeightAnimator != null
                            && !mHintAnimationRunning) || mPeekAnimator != null) {
                        mTouchSlopExceeded =
                                (mHeightAnimator != null && !mHintAnimationRunning)
                                        || mPeekAnimator != null || mTouchSlopExceededBeforeDown;
                        cancelHeightAnimator();
                        cancelPeek();
                        onTrackingStarted();
                    }
                    if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp()
                            && !mStatusBar.isBouncerShowing()) {
                        startOpening(event);
                    }
                    break;
                case MotionEvent.ACTION_POINTER_UP:
                    final int upPointer = event.getPointerId(event.getActionIndex());
                    if (mTrackingPointer == upPointer) {
                        // gesture is ongoing, find a new pointer to track
                        final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
                        final float newY = event.getY(newIndex);
                        final float newX = event.getX(newIndex);
                        mTrackingPointer = event.getPointerId(newIndex);
                        startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight);
                    }
                    break;
                case MotionEvent.ACTION_POINTER_DOWN:
                    if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
                        mMotionAborted = true;
                        endMotionEvent(event, x, y, true /* forceCancel */);
                        return false;
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    addMovement(event);
                    float h = y - mInitialTouchY;
                    // If the panel was collapsed when touching, we only need to check for the
                    // y-component of the gesture, as we have no conflicting horizontal gesture.
                    if (Math.abs(h) > getTouchSlop(event)
                            && (Math.abs(h) > Math.abs(x - mInitialTouchX)
                            || mIgnoreXTouchSlop)) {
                        mTouchSlopExceeded = true;
                        if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) {
                            if (!mJustPeeked && mInitialOffsetOnTouch != 0f) {
                                startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
                                h = 0;
                            }
                            cancelHeightAnimator();
                            // 向上滑动时,手指移动的距离达到一定的阈值会调用onTrackingStarted,
                            // 设置mTracking值为true,从而接收touch事件
                            onTrackingStarted();
                        }
                    }
                    float newHeight = Math.max(0, h + mInitialOffsetOnTouch);
                    if (newHeight > mPeekHeight) {
                        if (mPeekAnimator != null) {
                            mPeekAnimator.cancel();
                        }
                        mJustPeeked = false;
                    } else if (mPeekAnimator == null && mJustPeeked) {
                        // The initial peek has finished, but we haven't dragged as far yet, lets
                        // speed it up by starting at the peek height.
                        mInitialOffsetOnTouch = mExpandedHeight;
                        mInitialTouchY = y;
                        mMinExpandHeight = mExpandedHeight;
                        mJustPeeked = false;
                    }
                    newHeight = Math.max(newHeight, mMinExpandHeight);
                    if (-h >= getFalsingThreshold()) {
                        mTouchAboveFalsingThreshold = true;
                        mUpwardsWhenThresholdReached = isDirectionUpwards(x, y);
                    }
                    if (!mJustPeeked && (!mGestureWaitForTouchSlop || mTracking)
                            && !isTrackingBlocked()) {
                        // 用户滑动过程会调用setExpandedHeightInternal
                        setExpandedHeightInternal(newHeight);
                    }
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    addMovement(event);
                    endMotionEvent(event, x, y, false /* forceCancel */);
                    break;
            }
            return !mGestureWaitForTouchSlop || mTracking;
        }
    }

移动过程中:主要在调用了两个方法。

// PanelViewController.java
protected void onTrackingStarted() {
        endClosing();
        mTracking = true;
        mBar.onTrackingStarted();
        notifyExpandingStarted();
        notifyBarPanelExpansionChanged();
    }

// PanelViewController.java
public void setExpandedHeightInternal(float h) {
        if (isNaN(h)) {
            Log.wtf(TAG, "ExpandedHeight set to NaN");
        }
        if (mExpandLatencyTracking && h != 0f) {
            DejankUtils.postAfterTraversal(
                    () -> mLatencyTracker.onActionEnd(LatencyTracker.ACTION_EXPAND_PANEL));
            mExpandLatencyTracking = false;
        }
        float fhWithoutOverExpansion = getMaxPanelHeight() - getOverExpansionAmount();
        if (mHeightAnimator == null) {
            float overExpansionPixels = Math.max(0, h - fhWithoutOverExpansion);
            if (getOverExpansionPixels() != overExpansionPixels && mTracking) {
                setOverExpansion(overExpansionPixels, true /* isPixels */);
            }
            mExpandedHeight = Math.min(h, fhWithoutOverExpansion) + getOverExpansionAmount();
        } else {
            mExpandedHeight = h;
            if (mOverExpandedBeforeFling) {
                setOverExpansion(Math.max(0, h - fhWithoutOverExpansion), false /* isPixels */);
            }
        }
        // If we are closing the panel and we are almost there due to a slow decelerating
        // interpolator, abort the animation.
        if (mExpandedHeight < 1f && mExpandedHeight != 0f && mClosing) {
            mExpandedHeight = 0f;
            if (mHeightAnimator != null) {
                mHeightAnimator.end();
            }
        }
        mExpandedFraction = Math.min(1f,
                fhWithoutOverExpansion == 0 ? 0 : mExpandedHeight / fhWithoutOverExpansion);
        // 进行锁屏上的时间和通知View根据手指的移动距离进行缩小、变透明处理 
        onHeightUpdated(mExpandedHeight);
        notifyBarPanelExpansionChanged();
    }

下面主要从:onHeightUpdated、notifyBarPanelExpansionChanged 两方法作为入口。

先看 NotificationPanelViewController#onHeightUpdated()

// NotificationPanelViewController.java
    @Override
    protected void onHeightUpdated(float expandedHeight) {
        if (!mQsExpanded || mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted) {
            // 更新时钟位置将设置顶部填充,这可能会触发新的面板高度并重新定位时钟。
            // 这是一个循环依赖项,应该避免,否则会出现堆栈溢出。
            if (mStackScrollerMeasuringPass > 2) {
                if (DEBUG) Log.d(TAG, "Unstable notification panel height. Aborting.");
            } else {
                //锁屏上的时间和通知View根据手指的移动距离进行缩小、变透明处理
                positionClockAndNotifications();
            }
        }
        if (mQsExpandImmediate || mQsExpanded && !mQsTracking && mQsExpansionAnimator == null
                && !mQsExpansionFromOverscroll) {
            float t;
            if (mKeyguardShowing) {
                // 在Keyguard上,将QS扩展线性插值到面板扩展
                t = expandedHeight / (getMaxPanelHeight());
            } else {
                // In Shade, interpolate linearly such that QS is closed whenever panel height is
                // minimum QS expansion + minStackHeight
                float
                        panelHeightQsCollapsed =
                        mNotificationStackScroller.getIntrinsicPadding()
                                + mNotificationStackScroller.getLayoutMinHeight();
                float panelHeightQsExpanded = calculatePanelHeightQsExpanded();
                t =
                        (expandedHeight - panelHeightQsCollapsed) / (panelHeightQsExpanded
                                - panelHeightQsCollapsed);
            }
            float
                    targetHeight =
                    mQsMinExpansionHeight + t * (mQsMaxExpansionHeight - mQsMinExpansionHeight);
            setQsExpansion(targetHeight);
        }
        updateExpandedHeight(expandedHeight);
        updateHeader();
        // 更新通知半透明
        updateNotificationTranslucency();
        updatePanelExpanded();
        updateGestureExclusionRect();
        if (DEBUG) {
            mView.invalidate();
        }
    }

到这里了就一起看个滑动解锁的堆栈:

09-19 05:48:41.853  1477  1477 D yexiao  : java.lang.Throwable:
09-19 05:48:41.853  1477  1477 D yexiao  :      at com.android.keyguard.KeyguardSecurityContainer.showNextSecurityScreenOrFinish(KeyguardSecurityContainer.java:710)
09-19 05:48:41.853  1477  1477 D yexiao  :      at com.android.keyguard.KeyguardHostView.dismiss(KeyguardHostView.java:214)
09-19 05:48:41.853  1477  1477 D yexiao  :      at com.android.keyguard.KeyguardHostView.dismiss(KeyguardHostView.java:196)
09-19 05:48:41.853  1477  1477 D yexiao  :      at com.android.systemui.statusbar.phone.KeyguardBouncer.show(KeyguardBouncer.java:167)
09-19 05:48:41.853  1477  1477 D yexiao  :      at com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.showBouncer(StatusBarKeyguardViewManager.java:434)
09-19 05:48:41.853  1477  1477 D yexiao  :      at com.android.systemui.statusbar.phone.StatusBar.showBouncerIfKeyguard(StatusBar.java:3959)
09-19 05:48:41.853  1477  1477 D yexiao  :      at com.android.systemui.statusbar.phone.StatusBar.makeExpandedInvisible(StatusBar.java:2506)
09-19 05:48:41.853  1477  1477 D yexiao  :      at com.android.systemui.statusbar.phone.PhoneStatusBarView$1.run(PhoneStatusBarView.java:65)
09-19 05:48:41.853  1477  1477 D yexiao  :      at android.os.Handler.handleCallback(Handler.java:938)
09-19 05:48:41.853  1477  1477 D yexiao  :      at android.os.Handler.dispatchMessage(Handler.java:99)
09-19 05:48:41.853  1477  1477 D yexiao  :      at android.os.Looper.loop(Looper.java:223)
09-19 05:48:41.853  1477  1477 D yexiao  :      at android.app.ActivityThread.main(ActivityThread.java:7945)
09-19 05:48:41.853  1477  1477 D yexiao  :      at java.lang.reflect.Method.invoke(Native Method)
09-19 05:48:41.853  1477  1477 D yexiao  :      at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:603)
09-19 05:48:41.853  1477  1477 D yexiao  :      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

再看一个滑动后到密码安全锁(即密码解锁)的堆栈:

09-17 11:20:35.891  1473  1473 D yexiao: : java.lang.Throwable
09-17 11:20:35.891  1473  1473 D yexiao: :      at com.android.keyguard.KeyguardSecurityContainer.showNextSecurityScreenOrFinish(KeyguardSecurityContainer.java:710)
09-17 11:20:35.891  1473  1473 D yexiao: :      at com.android.keyguard.KeyguardHostView.dismiss(KeyguardHostView.java:214)
09-17 11:20:35.891  1473  1473 D yexiao: :      at com.android.keyguard.KeyguardHostView.dismiss(KeyguardHostView.java:196)
09-17 11:20:35.891  1473  1473 D yexiao: :      at com.android.systemui.statusbar.phone.KeyguardBouncer.show(KeyguardBouncer.java:167)
09-17 11:20:35.891  1473  1473 D yexiao: :      at com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.onPanelExpansionChanged(StatusBarKeyguardViewManager.java:297)
09-17 11:20:35.891  1473  1473 D yexiao: :      at com.android.systemui.statusbar.phone.PanelViewController.notifyBarPanelExpansionChanged(PanelViewController.java:1011)
09-17 11:20:35.891  1473  1473 D yexiao: :      at com.android.systemui.statusbar.phone.PanelViewController.setExpandedHeightInternal(PanelViewController.java:727)
09-17 11:20:35.891  1473  1473 D yexiao: :      at com.android.systemui.statusbar.phone.PanelViewController$TouchHandler.onTouch(PanelViewController.java:1338)
09-17 11:20:35.891  1473  1473 D yexiao: :      at com.android.systemui.statusbar.phone.NotificationPanelViewController$18.onTouch(NotificationPanelViewController.java:3229)
09-17 11:20:35.891  1473  1473 D yexiao: :      at android.view.View.dispatchTouchEvent(View.java:14385)
09-17 11:20:35.891  1473  1473 D yexiao: :      at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3120)
09-17 11:20:35.891  1473  1473 D yexiao: :      at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2792)
09-17 11:20:35.891  1473  1473 D yexiao: :      at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3126)
09-17 11:20:35.891  1473  1473 D yexiao: :      at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2806)
09-17 11:20:35.891  1473  1473 D yexiao: :      at com.android.systemui.statusbar.phone.NotificationShadeWindowView.dispatchTouchEvent(NotificationShadeWindowView.java:173)
09-17 11:20:35.891  1473  1473 D yexiao: :      at android.view.View.dispatchPointerEvent(View.java:14656)
// 省略部分Log....


相关文章
|
3月前
|
JavaScript Android开发
使用贝叶斯曲线滑动安卓屏幕(autojsPro7)
使用贝叶斯曲线滑动安卓屏幕(autojsPro7)
60 0
|
7月前
|
Java Linux Android开发
理解Android进程创建流程
理解Android进程创建流程
51 0
|
3月前
|
Android开发 Kotlin 索引
Android Compose——ScrollableTabRow和LazyColumn同步滑动
Android Compose——ScrollableTabRow和LazyColumn同步滑动
|
8月前
|
移动开发 Android开发
h5滑动底部兼容安卓
h5滑动底部兼容安卓
58 0
|
4月前
|
XML Java Android开发
Android App手势冲突处理中上下左右滑动的处理以及侧滑边缘菜单的讲解及实战(附源码 可直接使用)
Android App手势冲突处理中上下左右滑动的处理以及侧滑边缘菜单的讲解及实战(附源码 可直接使用)
66 0
|
4月前
|
XML Java Android开发
Android App开发触摸事件中手势事件Event的分发流程讲解与实战(附源码 简单易懂)
Android App开发触摸事件中手势事件Event的分发流程讲解与实战(附源码 简单易懂)
43 0
|
5月前
|
Android开发
Hbuilder打包android安装包流程
Hbuilder打包android安装包流程
|
7月前
|
XML Java Android开发
Android 仿抖音直播滑动清屏,完美解决滑动冲突
Android 仿抖音直播滑动清屏,完美解决滑动冲突
|
8月前
|
Android开发
Android 中ViewPager嵌套RecyclerView出现滑动冲突的解决方案
Android 中ViewPager嵌套RecyclerView出现滑动冲突的解决方案
697 0
|
8月前
|
Java Linux 开发工具
安卓 keystore 获得应用签名详细流程(快应用)
安卓 keystore 获得应用签名详细流程(快应用)
597 0