精华内容
下载资源
问答
  • view
    万次阅读
    2021-07-27 09:49:31

    view.post没执行,runOnUiThread,Handler

    坑点

    子线程执行view.post(Runnable) 部分 手机没有效果。

    usernameEditText.post(new Runnable() {
                        @Override
                        public void run() {
                            usernameEditText.setText("text set by runnable!");
                        }
                    });
    

    处理

    1. 使用handler.post
    2. 使用runOnUiThread

    原因

    低版本android基于ThreadLocal实现线程与数据关联,且线程的数据独立不共享。遇到多线程使用时,一个线程存储了数据,另一个线程取不到的数据的原因。
    子线程调用view.post时候,会构造一个队列存储到对应的线程数据空间,并将runnable加到此队列。当view要显示时候,ui线程会从ui线程的数据空间取出队列,遍历执行队列中的runnable,但是由于ThreadLocal的缘故,ui线程取到的队列肯定不包含子线程存到队列的runnable,所以这个runnable是不被执行的。因为刚才是子线程存的runnable,子线程可以取到,而UI线程并没有存我们期望的runnable,所以取不到。ThreadLocal特点就是线程之间的数据相互隔离,各自使用各自的数据,多线程使用时保证数据的“安全”。
    还没明白的话,这样讲一下:
    A、B钱包都没钱了,A从银行取了1000 人民币,装入了自己的钱包,B去商店买1000的商品,此时B从自己钱包里面拿钱时,钱包是空的。所以B是买不了商品的。

    7.0之前的系统存在这个问题,7.0之后已经被修复了。用的时候小心一点。

    经历

    给同事写了个程序,当时是一个子线程处理了数据之后调用view.post更新到界面上,自测是没问题的,结果同事那边告知不显示,当时也查看了源代码,同时也是反复验证没有问题,同事那里始终是有问题的,后来同事也没再提说。一段时间之后见了同事,问及此事,他说 只有他手机有问题,其他的手机没问题,所以就没再说这个事情了。让其掏出手机看了下,果真不显示,当即开始加日志调试,结果runnable代码块没有被执行,隐隐约约感觉到view.post不靠谱,直接在外层再加一个runOnUiThread之后,他的设备正常。 虽然问题是过去了,但一直没时间去弄清楚出现这个问题的原因,最近看到项目中有其他小伙伴也写了同样的代码,心里面有点慌。
    同时也查了些资料,总结记录之。

    复盘

    View.post()方法在android7.0之前,在view没有attachToWindow的时候调用该方法可能失效,尤其异步线程,如在onCreate,onBindViewHolder时调用view.post方法,可能会不生效,在异步线程view.post方法不执行的情况居多。建议使用Handler post方法代替。
    longlong2015 这里也对次问题进行说明

    于是乎,下载了一份6.0版本的sdk源码,以及9.0的源码进行对比,对比情况和引用文章差不多,也进一步对引用文章进行验证。

    6.0版本

    1. View的post函数
     public boolean post(Runnable action) {
            final AttachInfo attachInfo = mAttachInfo;
            if (attachInfo != null) {
                return attachInfo.mHandler.post(action);
            }
            // Assume that post will succeed later
            ViewRootImpl.getRunQueue().post(action);
            return true;
        }
    

    如果attachInfo有值,则是用attachInfo中的handler去post这个runnable,如果attachInfo没有值,则是ViewRootImpl.getRunQueue() 去执行post这个runnable。而attachInfo则是分别dispatchAttachedToWindow (首行)赋值的:

    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
            //System.out.println("Attached! " + this);
            mAttachInfo = info;
            if (mOverlay != null) {
                mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
            }
            mWindowAttachCount++;
            // We will need to evaluate the drawable state at least once.
            mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
            if (mFloatingTreeObserver != null) {
                info.mTreeObserver.merge(mFloatingTreeObserver);
                mFloatingTreeObserver = null;
            }
            if ((mPrivateFlags&PFLAG_SCROLL_CONTAINER) != 0) {
                mAttachInfo.mScrollContainers.add(this);
                mPrivateFlags |= PFLAG_SCROLL_CONTAINER_ADDED;
            }
            performCollectViewAttributes(mAttachInfo, visibility);
            onAttachedToWindow();
    
            ListenerInfo li = mListenerInfo;
            final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
                    li != null ? li.mOnAttachStateChangeListeners : null;
            if (listeners != null && listeners.size() > 0) {
                // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
                // perform the dispatching. The iterator is a safe guard against listeners that
                // could mutate the list by calling the various add/remove methods. This prevents
                // the array from being modified while we iterate it.
                for (OnAttachStateChangeListener listener : listeners) {
                    listener.onViewAttachedToWindow(this);
                }
            }
    
            int vis = info.mWindowVisibility;
            if (vis != GONE) {
                onWindowVisibilityChanged(vis);
            }
    
            // Send onVisibilityChanged directly instead of dispatchVisibilityChanged.
            // As all views in the subtree will already receive dispatchAttachedToWindow
            // traversing the subtree again here is not desired.
            onVisibilityChanged(this, visibility);
    
            if ((mPrivateFlags&PFLAG_DRAWABLE_STATE_DIRTY) != 0) {
                // If nobody has evaluated the drawable state yet, then do it now.
                refreshDrawableState();
            }
            needGlobalAttributesUpdate(false);
        }
    

    dispatchDetachedFromWindow(倒数第三行)中赋空

    void dispatchDetachedFromWindow() {
            AttachInfo info = mAttachInfo;
            if (info != null) {
                int vis = info.mWindowVisibility;
                if (vis != GONE) {
                    onWindowVisibilityChanged(GONE);
                }
            }
    
            onDetachedFromWindow();
            onDetachedFromWindowInternal();
    
            InputMethodManager imm = InputMethodManager.peekInstance();
            if (imm != null) {
                imm.onViewDetachedFromWindow(this);
            }
    
            ListenerInfo li = mListenerInfo;
            final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
                    li != null ? li.mOnAttachStateChangeListeners : null;
            if (listeners != null && listeners.size() > 0) {
                // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
                // perform the dispatching. The iterator is a safe guard against listeners that
                // could mutate the list by calling the various add/remove methods. This prevents
                // the array from being modified while we iterate it.
                for (OnAttachStateChangeListener listener : listeners) {
                    listener.onViewDetachedFromWindow(this);
                }
            }
    
            if ((mPrivateFlags & PFLAG_SCROLL_CONTAINER_ADDED) != 0) {
                mAttachInfo.mScrollContainers.remove(this);
                mPrivateFlags &= ~PFLAG_SCROLL_CONTAINER_ADDED;
            }
    
            mAttachInfo = null;
            if (mOverlay != null) {
                mOverlay.getOverlayView().dispatchDetachedFromWindow();
            }
        }
    
    1. ViewRootImpl.getRunQueue()
      ViewRootImpl 的静态成员 sRunQueues 和静态函数getRunQueue
    static final ThreadLocal<RunQueue> sRunQueues = new ThreadLocal<RunQueue>();
    static RunQueue getRunQueue() {
            RunQueue rq = sRunQueues.get();
            if (rq != null) {
                return rq;
            }
            rq = new RunQueue();
            sRunQueues.set(rq);
            return rq;
        }
    

    ui线程执行“存到队列中的任务"

     // Execute enqueued actions on every traversal in case a detached view enqueued an action
            getRunQueue().executeActions(mAttachInfo.mHandler);
    

    根源是sRunQueues.get(),其实也是ThreadLocal的特性。当子线程调用的时候,这里返回的rq 是空的,接着创建一个rt后存入。之后UI线程调用,这里返回的不是子线程创建的rq。

    1. ViewRootImpl.RunQueue.executeActions
     void executeActions(Handler handler) {
                synchronized (mActions) {
                    final ArrayList<HandlerAction> actions = mActions;
                    final int count = actions.size();
    
                    for (int i = 0; i < count; i++) {
                        final HandlerAction handlerAction = actions.get(i);
                        handler.postDelayed(handlerAction.action, handlerAction.delay);
                    }
    
                    actions.clear();
                }
            }
    
    1. ThreadLocal.get()
      再进一步看一下这个ThreadLocal的get实现(get的样子往往容易被忽视)
    public T get() {
            // Optimized for the fast path.
            Thread currentThread = Thread.currentThread();
            Values values = values(currentThread);
            if (values != null) {
                Object[] table = values.table;
                int index = hash & values.mask;
                if (this.reference == table[index]) {
                    return (T) table[index + 1];
                }
            } else {
                values = initializeValues(currentThread);
            }
    
            return (T) values.getAfterMiss(this);
        }
    

    好的,到这里已经看到取当前的线程做了一系列的事情,因此不同线程返回的自然就不一样。

    10.0版本

    1. View.post
    public boolean post(Runnable action) {
            final AttachInfo attachInfo = mAttachInfo;
            if (attachInfo != null) {
                return attachInfo.mHandler.post(action);
            }
    
            // Postpone the runnable until we know on which thread it needs to run.
            // Assume that the runnable will be successfully placed after attach.
            getRunQueue().post(action);
            return true;
        }
    

    可以看到这里以不是用ViewRootImpl.getRunQueue(),而是view内部的函数getRunQueue().

    1. View.getRunQueue()
    private HandlerActionQueue getRunQueue() {
            if (mRunQueue == null) {
                mRunQueue = new HandlerActionQueue();
            }
            return mRunQueue;
        }
    
    

    好家伙,现在的队列是属于view的了,不再是归属于线程,变成了共享变量。
    因此子线程向队列里面添加一个runnable之后,ui线程做来取队列就能取到。执行就是我们期望的结果了。

    1. View.dispatchAttachedToWindow
     void dispatchAttachedToWindow(AttachInfo info, int visibility) {
            mAttachInfo = info;
            if (mOverlay != null) {
                mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
            }
            mWindowAttachCount++;
            // We will need to evaluate the drawable state at least once.
            mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
            if (mFloatingTreeObserver != null) {
                info.mTreeObserver.merge(mFloatingTreeObserver);
                mFloatingTreeObserver = null;
            }
    
            registerPendingFrameMetricsObservers();
    
            if ((mPrivateFlags&PFLAG_SCROLL_CONTAINER) != 0) {
                mAttachInfo.mScrollContainers.add(this);
                mPrivateFlags |= PFLAG_SCROLL_CONTAINER_ADDED;
            }
            // Transfer all pending runnables.
            if (mRunQueue != null) {
                mRunQueue.executeActions(info.mHandler);
                mRunQueue = null;
            }
            performCollectViewAttributes(mAttachInfo, visibility);
            onAttachedToWindow();
    
            ListenerInfo li = mListenerInfo;
            final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
                    li != null ? li.mOnAttachStateChangeListeners : null;
            if (listeners != null && listeners.size() > 0) {
                // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
                // perform the dispatching. The iterator is a safe guard against listeners that
                // could mutate the list by calling the various add/remove methods. This prevents
                // the array from being modified while we iterate it.
                for (OnAttachStateChangeListener listener : listeners) {
                    listener.onViewAttachedToWindow(this);
                }
            }
    
            int vis = info.mWindowVisibility;
            if (vis != GONE) {
                onWindowVisibilityChanged(vis);
                if (isShown()) {
                    // Calling onVisibilityAggregated directly here since the subtree will also
                    // receive dispatchAttachedToWindow and this same call
                    onVisibilityAggregated(vis == VISIBLE);
                }
            }
    
            // Send onVisibilityChanged directly instead of dispatchVisibilityChanged.
            // As all views in the subtree will already receive dispatchAttachedToWindow
            // traversing the subtree again here is not desired.
            onVisibilityChanged(this, visibility);
    
            if ((mPrivateFlags&PFLAG_DRAWABLE_STATE_DIRTY) != 0) {
                // If nobody has evaluated the drawable state yet, then do it now.
                refreshDrawableState();
            }
            needGlobalAttributesUpdate(false);
    
            notifyEnterOrExitForAutoFillIfNeeded(true);
            notifyAppearedOrDisappearedForContentCaptureIfNeeded(true);
        }
    

    其中下面这段就是UI线程来执行存入需要处理的任务:

     if (mRunQueue != null) {
                mRunQueue.executeActions(info.mHandler);
                mRunQueue = null;
            }
    

    总结

    子线程在onAttachedToWindow之后调用view.post,是有效的。其次是与系统版本有一定关系,出现问题的场景就是子线程处理的完成数据之后调用view.post时,onAttachedToWindow还没有回调,一般是activity onCreate函数中初始化完成view之前这段时间可能出现不执行的问题。

    更多相关内容
  • 一、前言 ...本文先从 NavigationView 的基本应用开始,再补充如何灵活的使用 NavigationView 来完成很多更细节化的需求。 二、基本概念 如下所示,用一个 demo 展示了 NavigationView 和 Navig

    一、前言

    • 在 UIKit 的框架中,我们时常使用 UINavigationViewController 来管理页面的 push 和 pop,这是页面管理的基本操作。而到了 SwiftUI,该操作是交由 NavigationView 和 NavigationLink 来完成。
    • 本文先从 NavigationView 的基本应用开始,再补充如何灵活的使用 NavigationView 来完成很多更细节化的需求。

    二、基本概念

    • 如下所示,用一个 demo 展示了 NavigationView 和 NavigationLink 的基本应用:
    // NavigationView基础
    import SwiftUI
    
    @main
    struct iOS_testApp: App {
        var body: some Scene {
            WindowGroup {
                NavigationView {
                    NavigationLink(
                        destination: Text("Destination"),
                        label: {
                            Text("Navigate")
                        })
                }
            }
        }
    }
    
    • 在该示例中,提供了一个顶层 View,即 NavigationView,在 SwiftUI 中,NavigationView 相当于 UIKit 的 UINavigationViewController,它提供了整个页面导航环境的顶层容器,包裹在 NavigationView 下面的是 NavigationLink,它定义了本页面的视图以及待 push 的视图(通过点击)。
    • 如在示例中,Text(“Navigate”) 就是本页面的视图,而 Text(“Destination”) 就是点击跳转后的视图。主界面如下所示,点击 Navigate 即可 push:

    在这里插入图片描述

    • 点击 Navigate 后 push 新界面 Destination:

    在这里插入图片描述

    三、设置标题栏

    • 在 NavigationView 的默认展示设置中,根级界面是没有标题栏的,而待 push 的界面默认带标题返回栏,但是标题为空。通过 .navigationBarTitle 修饰属性可以对标题进行设置:
    // NavigationView根界面带标题栏
    import SwiftUI
    
    @main
    struct iOS_testApp: App {
        var body: some Scene {
            WindowGroup {
                NavigationView {
                    NavigationLink(
                        destination: Text("Destination"),
                        label: {
                            Text("Navigate")
                        })
                        .navigationBarTitle("Main", displayMode: .large)
                }
            }
        }
    }
    
    • 带 large 标题栏的 Navigate 界面,如下所示:

    在这里插入图片描述

    • 其中 displayMode 是一个枚举类型参数,支持 inline,large 和 automatic,分别表示小标题栏,大标题栏和自动选择,如果你选择 automatic,则一般系统会选择 large。

    四、隐藏标题栏

    • 某些情况下,如果不希望使用标题栏,或者不喜欢 NavigationView 提供的标题栏样式,对它提供的定制灵活性并不满意,而希望完全由自己接管和实现标题栏,在这种情况下,可以选择隐藏标题栏,隐藏标题栏通过 .navigationBarHidden(true) 来完成:
    // 隐藏destination标题栏
    import SwiftUI
    
    @main
    struct iOS_testApp: App {
        var body: some Scene {
            WindowGroup {
                NavigationView {
                    NavigationLink(
                        destination: Text("Destination")
                                        // 隐藏二级界面的标题栏
                                        .navigationBarHidden(true),
                        label: {
                            Text("Navigate")
                        })
                        .navigationBarTitle("Main", displayMode: .automatic)
                }
            }
        }
    }
    
    • 隐藏了标题栏的 Destination 界面,如下所示:

    在这里插入图片描述

    五、编程实现页面返回逻辑

    • 当隐藏了二级界面的标题栏后,我们岂不是把标题栏的返回按钮也隐藏了,那么要实现自己的返回按钮时,该怎么做呢?这时候就需要用到 SwiftUI 独有的机制:视图环境 @Environment,Environment 提供了视图共享的属性绑定服务,通过这些属性可以完成视图的基本操作,其中一个属性叫 presentationMode,该属性绑定了导航页面间的上下文关系,通过它的 dismiss 方法可以手动返回页面:
    // 通过编程实现页面返回逻辑
    import SwiftUI
    
    struct DestinationView: View {
        // 声明属性presentationMode
        @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
        var body: some View {
            Text("Destination View")
                .navigationBarHidden(true) // 追加后destination不再出现标题栏
                .onTapGesture {
                    // 点击"Destination View"后返回
                    self.presentationMode.wrappedValue.dismiss()
                }
        }
    }
    
    struct ContentView: View {
        var body: some View {
            NavigationView {
                NavigationLink(
                    destination: DestinationView(),
                    label: {
                        Text("Navigate View")
                    })
            }
        }
    }
    
    @main
    struct iOS_testApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
        }
    }
    
    • 注意:该方法在 iOS 15.0 后即将被属性 dismiss 替代,但是考虑到撰写本文时主流系统是 iOS 14.5,出于兼容需要,依然使用 presentationMode 来完成代码。
    • 在以上例子中,把 Text(“Destination”) 这个二级界面单独提取到 DestinationView 中, 也单独提出 ContentView。通过声明 @Environment(.presentationMode),让 DestinationView 获取了 presentationMode 属性的绑定数据。
    • 接下来给 Text(“Destination View”) 提供点击操作: onTapGesture,在点击的实现代码里调用 self.presentationMode.wrappedValue.dismiss() 。
    • 运行程序,现在可以通过点击 Navigate View 和 Destination View 自由往返。

    六、标题栏样式设置

    • 现在知道 NavigationView 提供了导航的基本元素,并且提供了系统默认的标题栏,我们可以隐藏标题栏从而自行设计界面。那么当我们想用默认的标题栏,但是想要改变其中的某些样式,比如标题颜色,应该怎么做呢?
    • 事实上,更改标题栏的样式在 SwiftUI 中属于全局配置,即配置一次后,对运行时间接下来的所有标题栏也生效,这个全局配置是通过 UINavigationBar.appearance() 来实现的。
    • 修改 ContentView 如下:
    // 设置标题栏标题为红色
    struct ContentView: View {
        var body: some View {
            NavigationView {
                NavigationLink(
                    destination: DestinationView(),
                    label: {
                        Text("Navigate View")
                    })
                    .navigationBarTitle("Title", displayMode: .inline)
                    .onAppear() {
                        // 设置标题颜色
                        UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.red]
                    }
            }
        }
    }
    
    • 如下所示,Navigate View 标题栏的标题为红色,样式为 inline:

    在这里插入图片描述

    • 运行应用程序,可以发现 title 是红色的,与此同时,该设置对 DestinationView 也同样有效:
    // 展示DestinationView标题,一样发现是红色
    struct DestinationView: View {
        @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
        var body: some View {
            Text("Destination View")
                .navigationBarHidden(false) // 标题栏不隐藏
                .navigationTitle("Title 2") // 追加标题
                .onTapGesture {
                    self.presentationMode.wrappedValue.dismiss()
                }
        }
    }
    
    • Destination View 标题栏设置为红色:

    在这里插入图片描述

    七、进阶:去掉点击的交互特效

    • 当运行示例程序时,很容易会发现点击 Navigate View 会出现一个明显的渐变特效,另一方面,也很容易发现 Navigate View 的字体颜色是经典的 iOS 7 蓝,这是默认的按钮效果,对于这种效果有些人觉得很好,但是对于开发者开发的应用,由于界面风格的不同,该特效并不是什么时候都是合适的。如果想移除这个效果,该怎么做?
    • Navigate View 被按下时的默认展示效果,如下:

    在这里插入图片描述

    • 这时候就要用到 buttonStyle 修饰器,在 SwiftUI 中,它的完整声明如下:
    // buttonStyle的声明(不是我写的)
    extension View {
        public func buttonStyle<S>(_ style: S) -> some View where S : ButtonStyle
    }
    
    • 通过该修饰器来完成 Button 样式的修改,而传入的参数 ButtonStyle 由自己定义。也就是说,在此之前需要定义一个 ButtonStyle 的 Struct,代码如下:
    // 定义一个ButtonStyle,命名为DefaultButtonStyle
    struct DefaultButtonStyle: ButtonStyle {
        func makeBody(configuration: Self.Configuration) -> some View {
          configuration.label
            .background(configuration.isPressed ? Color.clear : Color.clear)
        }
    }
    
    • 在本例中,把背景颜色全部改成了.clear,开发者可以根据自身需求修改。并且 configuration的isPressed 状态属性也很有用,可以根据状态改变按钮视觉。接下来在 ContentView 中设置 buttonStyle:
    // 设置buttonStyle
    struct ContentView: View {
        var body: some View {
            NavigationView {
                NavigationLink(
                    destination: DestinationView(),
                    label: {
                        Text("Navigate View")
                    })
                    .navigationBarTitle("Title", displayMode: .inline)
                    // 设置按钮样式
                    .buttonStyle(DefaultButtonStyle())
                    .onAppear() {
                        UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.red]
                    }
            }
        }
    }
    
    • 运行应用程序,就会发现按钮样式已经不再有原先的样式特效,消除了默认特效后的"Navigate View"如下所示:

    在这里插入图片描述

    八、进阶:支持默认点击之外的更多交互

    • 到目前为止,NavigationView 和 NavigationLink 已经可以满足我们日常开发的大部分需求了。但是,在某些情况下,我们对产品的交互有更丰富的需求。例如,在本例中,NavigationLink 默认支持点击操作,但是如果我们想要更多的操作响应怎么办,比如长按响应?
    • 开始进行尝试,先把 ContentView 进行简化,去掉原先追加的若干代码,然后加入 onLongPressGesture:
    // 尝试加入onLongPressGesture
    struct ContentView: View {
        var body: some View {
            NavigationView {
                NavigationLink(
                    destination: DestinationView(),
                    label: {
                        Text("Navigate View")
                            .onLongPressGesture {
                                print("long press")
                            }
                    })
            }
        }
    }
    
    • 运行代码,可以发现,当长按 Navigate View 时,确实打印出了"long press",但是同时 NavigationLink 的点击响应也失效了,这明显不符合我们需求。原因在于支持了 onLongPressGesture,NavigationLink 的按钮属性也被更高优先级的 gesture 取代,按钮点击功能不再有效。
    • 如何既支持长按,又支持点击呢?这里提供的一个方案是,加入 onTap 操作:
    // 通过onTapGesture来支持点击响应
    struct ContentView: View {
        var body: some View {
            NavigationView {
                NavigationLink(
                    destination: DestinationView(),
                    label: {
                        Text("Navigate View")
                            .onTapGesture {
                                print("tap")
                            }
                            .onLongPressGesture {
                                print("long press")
                            }
                    })
            }
        }
    }
    
    • 再次运行代码,这时候可以发现"tap"和"long press"都可以正确打印。

    九、isActive 参数

    • 在之前的代码中,已经可以用 onTapGesture 和 onLongPressGesture 来分别响应 NavigationLink 的交互,但是也发现了一个问题。NavigationLink 最重要的跳转问题,还没有得到解决。现在引进一个重要参数隆重登场:isActive,它是 NavigationLink 构造函数的一个参数,默认值为 .constant(true),先来看看它的正确使用方法:
    // 引入了isActive来手动跳转
    struct ContentView: View {
        @State private var isActive = false // 定义isActive状态,默认为false
        var body: some View {
            NavigationView {
                NavigationLink(
                    destination: DestinationView(),
                    isActive: $isActive, // 绑定isActive
                    label: {
                        Text("Navigate View")
                            .onTapGesture {
                                print("tap")
                                isActive = true // 点击的时候,设置为true触发跳转
                            }
                            .onLongPressGesture {
                                print("long press")
                            }
                    })
            }
        }
    }
    
    • isActive 是 NavigationLink 插入二级页面的触发参数,如果它是个常量,为 false 时则不会触发,为 true 时则在点击的时候触发。但是如果参数是一个 @State 变量,则是由 @State 的变量值来决定是否插入二级页面。
    • 在以上代码里,定义了名为 isActive 的 @State 变量,初始值为 false,并且将它绑定到 NavigationLink的isActive 参数中,当用户点击"Navigate View"时,触发 onTapGesture,在其实现代码中,设置 isActive 为 true,成功触发 destination 的载入操作,这时候如预期的加载 DestinationView。

    十、更复杂案例:多个 NavigationLink 下的情况

    • 在实战项目中,还会遇到更多关于 NavigationView 的挑战,但是方法总比问题多,总有应对之策。刚刚提到一个 @State 变量 isActive 可以解决由编程决定载入页面的问题。但是在项目实践中,往往有多个 NavigationLink,它们或者由 VStack 组成,或者由 ScrollView 组成。在这种情况下,一个 isActive 变量完全不够用。一个不够,就出动多个,用一个数组来解决问题。
    • 现在用一个完整的程序代码来展示下用法:
    // 通过数组控制页面的导航
    import SwiftUI
    
    struct ContentView: View {
        // 用数组替代单一的变量
        @State private var isActives: [Bool] = Array(repeating: false, count: 2)
        var body: some View {
            NavigationView {
                VStack {
                    NavigationLink(
                        destination: Text("Destination View 1"),
                        isActive: $isActives[0],
                        label: {
                            Text("Navigate View 1")
                                .onTapGesture {
                                    print("tap 1")
                                    isActives[0] = true
                                }
                                .onLongPressGesture {
                                    print("long press 1")
                                }
                        })
                    NavigationLink(
                        destination: Text("Destination View 2"),
                        isActive: $isActives[1],
                        label: {
                            Text("Navigate View 2")
                                .onTapGesture {
                                    print("tap 2")
                                    isActives[1] = true
                                }
                                .onLongPressGesture {
                                    print("long press 2")
                                }
                        })
                }
            }
        }
    }
    
    @main
    struct iOS_testApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
        }
    }
    
    • 运行程序,可以看到"Destination View 1"和"Destination View 2"都可以很好的响应点击、长按等交互:

    在这里插入图片描述

    十一、进阶技巧:NavigationLink 数目可变条件下的编程

    • 通过 isActives 数组控制 NavigationLink 跳转的思路虽然是对的,但是示例代码并不能解决实际项目中的需求。因为在样例中 isActives 数组数目是已知的固定的,而在实际项目中,NavigationLink 数目可能是动态下发的,这种情况下该如何编码呢?
    • 下面来看看,一个典型的根据数组元素构建的 NavigationLink 是如何编写的:
    // 由数组items决定NavigationLink数量
    struct ContentView: View {
        @State private var items: [Int] = []
        
        var body: some View {
            NavigationView {
                ScrollView {
                    ForEach(items, id: \.self) { item in
                        NavigationLink(
                            destination: Text("Destination View \(item)"),
                            label: {
                                Text("Navigate View \(item)")
                            })
                    }
                }
            }
            .onAppear() {
                items = Array(arrayLiteral: 1, 2)
            }
        }
    }
    
    • 在以上代码中,NavigationLink 由 items 动态决定,而不是一段一段的写死,通过 ForEach 来逐个创建 NavigationLink,那么问题来了:如果在这种情况下实现原先的点击/长按需求,该怎么做?
    • 解决方法有很多,在这里提供一个我的解决方案,代码如下:
    // 动态的isActives数组完成状态绑定
    struct ContentView: View {
        @State private var isActives: [Bool] = []
        @State private var items: [Int] = []
        
        var body: some View {
            NavigationView {
                ScrollView {
                    ForEach(items, id: \.self) { item in
                        NavigationLink(
                            destination: Text("Destination View \(item)"),
                            isActive: $isActives[self.items.firstIndex(of: item)!], // 正确的绑定item所对应的isActive数组位置
                            label: {
                                Text("Navigate View \(item)")
                                    .onTapGesture {
                                        print("tap \(item)")
                                        isActives[self.items.firstIndex(of: item)!] = true // 点击的时候,获取正确的数组下标并修改绑定值
                                    }
                                    .onLongPressGesture {
                                        print("long press \(item)")
                                    }
                            }
                        )
                    }
                }
            }
            .onAppear() {
                items = Array(arrayLiteral: 1, 2)
                isActives = Array(repeating: false, count: items.count) // 动态创建isActives数组,和items数目保持一致
            }
        }
    }
    
    • 以上代码实现了用数组 isActives 动态绑定了每一个 NavigationLink 的 isActive 属性。在以上实现过程中,需要注意:
      • 通过在 onAppear 下的代码,动态创建 isActives 数组,数组的个数和 items 数目保持一致:
    isActives = Array(repeating: false, count: items.count)
    
      • 在 ScrollView 循环创建 NavigationLink 的 ForEach 中,通过以下方式获得正确的下标:
    self.items.firstIndex(of: item)!
    
      • 把 $isActives[self.items.firstIndex(of: item)!] 绑定到 isActive 参数中;
      • 在点击事件中,将绑定的数组元素设置为 true;
    isActives[self.items.firstIndex(of: item)!] = true
    
    展开全文
  • Android自定义View

    千次阅读 多人点赞 2020-08-02 14:28:29
    为什么要自定义View 自定义View的基本方法 自定义View的最基本的三个方法分别是: onMeasure()、onLayout()、onDraw(); View在Activity中显示出来,要经历测量、布局和绘制三个步骤,分别对应三个动作:measure、...

    概述

    Android开发进阶的必经之路
    为什么要自定义View
    自定义View的基本方法

    自定义View的最基本的三个方法分别是: onMeasure()、onLayout()、onDraw(); View在Activity中显示出来,要经历测量、布局和绘制三个步骤,分别对应三个动作:measure、layout和draw。

    1. 测量:onMeasure()决定View的大小;
    2. 布局:onLayout()决定View在ViewGroup中的位置;
    3. 绘制:onDraw()决定绘制这个View。

    自定义控件分类

    1. 自定义View: 只需要重写onMeasure()和onDraw(),在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View
    2. 自定义ViewGroup: 则只需要重写onMeasure()和onLayout(),自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout

    自定义View基础

    视图View主要分为以下两类:

    类别解释特点
    单一视图即一个View,如TextView不包含子View
    视图组即多个View组成的ViewGroup,如LinearLayout包含子View

    View类简介

    1. View类是Android中各种组件的基类,如View是ViewGroup基类,ViewGroup是继承自View类的,但是在视图组中,ViewGroup是父组件,ViewGroup父组件中会包含多个子View
    2. View表现为显示在屏幕上的各种视图

    Android中的UI都是有View和ViewGroup组成的

    View的构造函数有4个:

    // 如果View是在Java代码里面new的,则调用第一个构造函数
    public CarsonView(Context context) {
        super(context);
    }
    
    // 如果View是在.xml里声明的,则调用第二个构造函数
    // 自定义属性是从AttributeSet参数传进来的
    // 这个方法一般是必须重写的,因为在LayoutInfaltor中CreateView的时候,系统会通过反射调用该构造函数,如果没有重写创建View的时候会报错
    public  CarsonView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    
    // 不会自动调用
    // 一般是在第二个构造函数里主动调用
    // 如View有style属性时
    public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    
    //API21之后才使用
    // 不会自动调用
    // 一般是在第二个构造函数里主动调用
    // 如View有style属性时
    public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr, intdefStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    View的创建绘制流程:

     

    AttributeSet与自定义属性

    系统自带的View可以在xml中配置属性,对于写的好的自定义View同样可以在xml中配置属性,为了使自定义的View的属性可以在xml中配置,需要以下4个步骤:
    1. 通过 <declare-styleable> 为自定义View添加属性
    2. 在xml中为相应的属性声明属性值
    3. 在运行时(一般为构造函数)获取属性值
    4. 将获取到的属性值应用到View

    View视图结构

    1. PhoneWindow是Android系统中最基本的窗口系统,继承自Windows类,负责管理界面显示以及事件响应。它是Activity与View系统交互的接口
    2. DecorView是PhoneWindow中的起始节点View,继承于View类,作为整个视图容器来使用。用于设置窗口属性。它本质上是一个FrameLayout,DecorView是继承自FrameLayout的
    3. ViewRoot在Activtiy启动时创建,负责管理、布局、渲染窗口UI等等

    对于多View的视图,结构是树形结构:最顶层是ViewGroup,ViewGroup下可能有多个ViewGroup或View,如下
    图:

    一定要记住:Android系统无论是measure过程、layout过程还是draw过程,永远都是从View树的根节点开始测量或计算(即从
    树的顶端开始),一层一层、一个分支一个分支地进行(即树形递归),最终计算整个View树中各个View,最终确
    定整个View树的相关属性。

    Android坐标系

    Android的坐标系定义为:

    • 屏幕的左上角为坐标原点
    • 向右为x轴增大方向
    • 向下为y轴增大方向

    区别于一般的数学坐标系:

    View位置(坐标)描述

    View的位置由4个顶点决定的, 4个顶点的位置描述分别由4个值决定,请记住:View的位置是相对于父控件而言的

    • Top:子View上边界到父view上边界的距离
    • Left:子View左边界到父view左边界的距离
    • Bottom:子View下边距到父View上边界的距离
    • Right:子View右边界到父view左边界的距离

    位置获取方式

    View的位置是通过view.getxxx()函数进行获取:(以Top为例)

    // 获取Top位置
    public final int getTop() { 
      return mTop; 
    } 
    // 其余如下:
    getLeft();    //获取子View左上角距父View左侧的距离
    getBottom();   //获取子View右下角距父View顶部的距离
    getRight();   //获取子View右下角距父View左侧的距离

    与MotionEvent中 get()和getRaw()的区别

    //get() :触摸点相对于其所在组件坐标系的坐标
    event.getX();   
    event.getY();
    //getRaw() :触摸点相对于屏幕默认坐标系的坐标
    event.getRawX();  
    event.getRawY();

    getX()和getRawX()的区别参照下图:

    getMeasureWidth与getWidth的区别

    getMeasureWidth

     

    1. 在measure()过程结束后就可以获取到对应的值;
    2. 通过setMeasuredDimension()方法来进行设置的.

    getWidth

     

    1. 在layout()过程结束后才能获取到;
    2. 通过视图右边的坐标减去左边的坐标计算出来的.

    Android中颜色相关内容

    Android支持的颜色模式:以ARGB8888为例

    View树的绘制流程

    View树的绘制流程是谁负责的?
    view树的绘制流程是通过ViewRoot去负责绘制的,ViewRoot这个类的命名有点坑,最初看到这个名字,翻译过来是
    view的根节点,但是事实完全不是这样,ViewRoot其实不是View的根节点,它连view节点都算不上,它的主要作用
    是View树的管理者,负责将DecorView和PhoneWindow“组合”起来,而View树的根节点严格意义上来说只有
    DecorView;每个DecorView都有一个ViewRoot与之关联,这种关联关系是由WindowManager去进行管理的;

    View绘制流程如下图:

    View的添加

    View的绘制流程

    measure


    1. 系统为什么要有measure过程?
    2. measure过程都干了点什么事?
    3. 对于自适应的尺寸机制,如何合理的测量一颗View树?
    4. 那么ViewGroup是如何向子View传递限制信息的?
    5. ScrollView嵌套ListView问题?

    Layout


    1. 系统为什么要有layout过程?
    2. layout过程都干了点什么事?

    Draw


    1. 系统为什么要有draw过程?
    2. draw过程都干了点什么事

    LayoutParams


    LayoutParams翻译过来就是布局参数,子View通过LayoutParams告诉父容器(ViewGroup)应该如何放置自己。
    从这个定义中也可以看出来LayoutParams与ViewGroup是息息相关的,因此脱离ViewGroup谈LayoutParams是没
    有意义的。
    事实上,每个ViewGroup的子类都有自己对应的LayoutParams类,典型的如LinearLayout.LayoutParams和
    FrameLayout.LayoutParams等,可以看出来LayoutParams都是对应ViewGroup子类的内部类


    MarginLayoutParams


    MarginLayoutParams是和外间距有关的。事实也确实如此,和LayoutParams相比,MarginLayoutParams只是增
    加了对上下左右外间距的支持。实际上大部分LayoutParams的实现类都是继承自MarginLayoutParams,因为基本
    所有的父容器都是支持子View设置外间距的
    属性优先级问题 MarginLayoutParams主要就是增加了上下左右4种外间距。在构造方法中,先是获取了
    margin属性;如果该值不合法,就获取horizontalMargin;如果该值不合法,再去获取leftMargin和
    rightMargin属性(verticalMargin、topMargin和bottomMargin同理)。我们可以据此总结出这几种属性的优
    先级
    margin > horizontalMargin和verticalMargin > leftMargin和RightMargin、topMargin和bottomMargin
    属性覆盖问题 优先级更高的属性会覆盖掉优先级较低的属性。此外,还要注意一下这几种属性上的注释
    Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value

    LayoutParams与View如何建立联系

    • 在XML中定义View
    • 在Java代码中直接生成View对应的实例对象

    addView

    /**
    * 重载方法1:添加一个子View
    * 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
     */
    public void addView(View child) {
      addView(child, -1);
    }
    /**
    * 重载方法2:在指定位置添加一个子View
    * 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
    * @param index View将在ViewGroup中被添加的位置(-1代表添加到末尾)
    */
    public void addView(View child, int index) {
      if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
     }
      LayoutParams params = child.getLayoutParams();
      if (params == null) {
        params = generateDefaultLayoutParams();// 生成当前ViewGroup默认的LayoutParams
        if (params == null) {
          throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return
    null");
       }
     }
      addView(child, index, params);
    }
    /**
    * 重载方法3:添加一个子View
    * 使用当前ViewGroup默认的LayoutParams,并以传入参数作为LayoutParams的width和height
    */
    public void addView(View child, int width, int height) {
      final LayoutParams params = generateDefaultLayoutParams();  // 生成当前ViewGroup默认的
    LayoutParams
      params.width = width;
      params.height = height;
      addView(child, -1, params);
    }
    /**
    * 重载方法4:添加一个子View,并使用传入的LayoutParams
    */
    @Override
    public void addView(View child, LayoutParams params) {
      addView(child, -1, params);
    }
    /**
    * 重载方法4:在指定位置添加一个子View,并使用传入的LayoutParams
    */
    public void addView(View child, int index, LayoutParams params) {
      if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
     }
    // addViewInner() will call child.requestLayout() when setting the new LayoutParams
      // therefore, we call requestLayout() on ourselves before, so that the child's request
      // will be blocked at our level
      requestLayout();
      invalidate(true);
      addViewInner(child, index, params, false);
    }
    private void addViewInner(View child, int index, LayoutParams params,
        boolean preventRequestLayout) {
     .....
      if (mTransition != null) {
        mTransition.addChild(this, child);
     }
      if (!checkLayoutParams(params)) { // ① 检查传入的LayoutParams是否合法
        params = generateLayoutParams(params); // 如果传入的LayoutParams不合法,将进行转化操作
     }
      if (preventRequestLayout) { // ② 是否需要阻止重新执行布局流程
        child.mLayoutParams = params; // 这不会引起子View重新布局(onMeasure->onLayout-
    >onDraw)
     } else {
        child.setLayoutParams(params); // 这会引起子View重新布局(onMeasure->onLayout-
    >onDraw)
     }
      if (index < 0) {
        index = mChildrenCount;
     }
      addInArray(child, index);
      // tell our children
      if (preventRequestLayout) {
        child.assignParent(this);
     } else {
        child.mParent = this;
     }
     .....
    }

    自定义LayoutParams

    1. 创建自定义属性

    <resources>
      <declare-styleable name="xxxViewGroup_Layout">
        <!-- 自定义的属性 -->
        <attr name="layout_simple_attr" format="integer"/>
        <!-- 使用系统预置的属性 -->
        <attr name="android:layout_gravity"/>
      </declare-styleable>
    </resources>

    2. 继承MarginLayout

    public static class LayoutParams extends ViewGroup.MarginLayoutParams {
      public int simpleAttr;
      public int gravity;
      public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
        // 解析布局属性
        TypedArray typedArray = c.obtainStyledAttributes(attrs,
    R.styleable.SimpleViewGroup_Layout);
        simpleAttr =
    typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_layout_simple_attr, 0);
      
     gravity=typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_android_layout_gravity,
    -1);
        typedArray.recycle();//释放资源
     }
      public LayoutParams(int width, int height) {
        super(width, height);
     }
      public LayoutParams(MarginLayoutParams source) {
        super(source);
     }
      public LayoutParams(ViewGroup.LayoutParams source) {
        super(source);
     }
    }

    3. 重写ViewGroup中几个与LayoutParams相关的方法

    // 检查LayoutParams是否合法
    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
      return p instanceof SimpleViewGroup.LayoutParams;
    }
    // 生成默认的LayoutParams
    @Override
    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
      return new SimpleViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
    LayoutParams.WRAP_CONTENT);
    }
    // 对传入的LayoutParams进行转化
    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
      return new SimpleViewGroup.LayoutParams(p);
    }
    // 对传入的LayoutParams进行转化
    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
      return new SimpleViewGroup.LayoutParams(getContext(), attrs);
    }

    LayoutParams常见的子类

    在为View设置LayoutParams的时候需要根据它的父容器选择对应的LayoutParams,否则结果可能与预期不一致,
    这里简单罗列一些常见的LayoutParams子类:

    • ViewGroup.MarginLayoutParams
    • FrameLayout.LayoutParams
    • LinearLayout.LayoutParams
    • RelativeLayout.LayoutParams
    • RecyclerView.LayoutParams
    • GridLayoutManager.LayoutParams
    • StaggeredGridLayoutManager.LayoutParams
    • ViewPager.LayoutParams
    • WindowManager.LayoutParams

    MeasureSpec

    测量规格,封装了父容器对 view 的布局上的限制,内部提供了宽高的信息( SpecMode 、 SpecSize ),SpecSize是指
    在某种SpecMode下的参考尺寸,其中SpecMode 有如下三种:

    • UNSPECIFIED 父控件不对你有任何限制,你想要多大给你多大,想上天就上天。这种情况一般用于系统内部,表示一种测量状态。(这个模式主要用于系统内部多次Measure的情形,并不是真的说你想要多大最后就真有多大)
    • EXACTLY 父控件已经知道你所需的精确大小,你的最终大小应该就是这么大。
    • AT_MOST 你的大小不能大于父控件给你指定的size,但具体是多少,得看你自己的实现。

    MeasureSpecs 的意义

    通过将 SpecMode 和 SpecSize 打包成一个 int 值可以避免过多的对象内存分配,为了方便操作,其提供了打包 / 解
    包方法

    MeasureSpec值的确定

    MeasureSpec值到底是如何计算得来的呢?

    子View的MeasureSpec值是根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具
    体计算逻辑封装在ViewGroup的getChildMeasureSpec()方法里

    /**
      *
      * 目标是将父控件的测量规格和child view的布局参数LayoutParams相结合,得到一个
      * 最可能符合条件的child view的测量规格。 
      * @param spec 父控件的测量规格
      * @param padding 父控件里已经占用的大小
      * @param childDimension child view布局LayoutParams里的尺寸
      * @return child view 的测量规格
      */
      public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec); //父控件的测量模式
        int specSize = MeasureSpec.getSize(spec); //父控件的测量大小
        int size = Math.max(0, specSize - padding);
        int resultSize = 0;
        int resultMode = 0;
        switch (specMode) {
        // 当父控件的测量模式 是 精确模式,也就是有精确的尺寸了
        case MeasureSpec.EXACTLY:
          //如果child的布局参数有固定值,比如"layout_width" = "100dp"
          //那么显然child的测量规格也可以确定下来了,测量大小就是100dp,测量模式也是EXACTLY
          if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
         }
          //如果child的布局参数是"match_parent",也就是想要占满父控件
    //而此时父控件是精确模式,也就是能确定自己的尺寸了,那child也能确定自己大小了
          else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
         }
          //如果child的布局参数是"wrap_content",也就是想要根据自己的逻辑决定自己大小,
          //比如TextView根据设置的字符串大小来决定自己的大小
          //那就自己决定呗,不过你的大小肯定不能大于父控件的大小嘛
          //所以测量模式就是AT_MOST,测量大小就是父控件的size
          else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
         }
          break;
        // 当父控件的测量模式 是 最大模式,也就是说父控件自己还不知道自己的尺寸,但是大小不能超过size
        case MeasureSpec.AT_MOST:
          //同样的,既然child能确定自己大小,尽管父控件自己还不知道自己大小,也优先满足孩子的需求??
          if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
         }
          //child想要和父控件一样大,但父控件自己也不确定自己大小,所以child也无法确定自己大小
          //但同样的,child的尺寸上限也是父控件的尺寸上限size
          else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
         }
          //child想要根据自己逻辑决定大小,那就自己决定呗
          else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
         }
          break;
        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
          if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
         } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = 0;
            resultMode = MeasureSpec.UNSPECIFIED;
         } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = 0;
            resultMode = MeasureSpec.UNSPECIFIED;
         }
          break;
     }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
     }

    针对上表,这里再做一下具体的说明
    对于应用层 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定
    对于不同的父容器和view本身不同的LayoutParams,view就可以有多种MeasureSpec。 1. 当view采用固定宽
    高的时候,不管父容器的MeasureSpec是什么,view的MeasureSpec都是精确模式并且其大小遵循
    Layoutparams中的大小; 2. 当view的宽高是match_parent时,这个时候如果父容器的模式是精准模式,那么
    view也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式,那么view也是最大模式并且其大
    小不会超过父容器的剩余空间; 3. 当view的宽高是wrap_content时,不管父容器的模式是精准还是最大化,
    view的模式总是最大化并且大小不能超过父容器的剩余空间。 4. Unspecified模式,这个模式主要用于系统内
    部多次measure的情况下,一般来说,我们不需要关注此模式(这里注意自定义View放到ScrollView的情况 需要
    处理)。

    展开全文
  • Android 自定义View,实现折线图

    千次下载 热门讨论 2014-06-10 22:03:04
    Android 自定义View,实现折线图 ,可参考博客http://blog.csdn.net/yifei1989/article/details/29891211
  • traceview.bat

    千次下载 热门讨论 2017-03-10 11:42:22
    下载后放到你的SDK\tools下,重启eclipse就好了。如果你有其他的SDK,里面有该文件,直接复制进去也可以的。
  • Android 自定义View (一)

    万次阅读 多人点赞 2014-04-21 15:20:04
    很多的Android入门程序猿来说对于Android自定义View,可能都是比较恐惧的,但是这又是高手进阶的必经之路,所有准备在自定义View上面花一些功夫,多写一些文章。先总结下自定义View的步骤: 1、自定义View的属性 2、...

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24252901

    很多的Android入门程序猿来说对于Android自定义View,可能都是比较恐惧的,但是这又是高手进阶的必经之路,所有准备在自定义View上面花一些功夫,多写一些文章。先总结下自定义View的步骤:

    1、自定义View的属性

    2、在View的构造方法中获得我们自定义的属性

    [ 3、重写onMesure ]

    4、重写onDraw

    我把3用[]标出了,所以说3不一定是必须的,当然了大部分情况下还是需要重写的。

    1、自定义View的属性,首先在res/values/  下建立一个attrs.xml , 在里面定义我们的属性和声明我们的整个样式。

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    
        <attr name="titleText" format="string" />
        <attr name="titleTextColor" format="color" />
        <attr name="titleTextSize" format="dimension" />
    
        <declare-styleable name="CustomTitleView">
            <attr name="titleText" />
            <attr name="titleTextColor" />
            <attr name="titleTextSize" />
        </declare-styleable>
    
    </resources>
    我们定义了字体,字体颜色,字体大小3个属性,format是值该属性的取值类型:

    一共有:string,color,demension,integer,enum,reference,float,boolean,fraction,flag;不清楚的可以google一把。

    然后在布局中声明我们的自定义View

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:custom="http://schemas.android.com/apk/res/com.example.customview01"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    
        <com.example.customview01.view.CustomTitleView
            android:layout_width="200dp"
            android:layout_height="100dp"
            custom:titleText="3712"
            custom:titleTextColor="#ff0000"
            custom:titleTextSize="40sp" />
    
    </RelativeLayout>

    一定要引入 xmlns:custom="http://schemas.android.com/apk/res/com.example.customview01"我们的命名空间,后面的包路径指的是项目的package

    2、在View的构造方法中,获得我们的自定义的样式

    /**
    	 * 文本
    	 */
    	private String mTitleText;
    	/**
    	 * 文本的颜色
    	 */
    	private int mTitleTextColor;
    	/**
    	 * 文本的大小
    	 */
    	private int mTitleTextSize;
    
    	/**
    	 * 绘制时控制文本绘制的范围
    	 */
    	private Rect mBound;
    	private Paint mPaint;
    
    	public CustomTitleView(Context context, AttributeSet attrs)
    	{
    		this(context, attrs, 0);
    	}
    
    	public CustomTitleView(Context context)
    	{
    		this(context, null);
    	}
    
    	/**
    	 * 获得我自定义的样式属性
    	 * 
    	 * @param context
    	 * @param attrs
    	 * @param defStyle
    	 */
    	public CustomTitleView(Context context, AttributeSet attrs, int defStyle)
    	{
    		super(context, attrs, defStyle);
    		/**
    		 * 获得我们所定义的自定义样式属性
    		 */
    		TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomTitleView, defStyle, 0);
    		int n = a.getIndexCount();
    		for (int i = 0; i < n; i++)
    		{
    			int attr = a.getIndex(i);
    			switch (attr)
    			{
    			case R.styleable.CustomTitleView_titleText:
    				mTitleText = a.getString(attr);
    				break;
    			case R.styleable.CustomTitleView_titleTextColor:
    				// 默认颜色设置为黑色
    				mTitleTextColor = a.getColor(attr, Color.BLACK);
    				break;
    			case R.styleable.CustomTitleView_titleTextSize:
    				// 默认设置为16sp,TypeValue也可以把sp转化为px
    				mTitleTextSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
    						TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
    				break;
    
    			}
    
    		}
    		a.recycle();
    
    		/**
    		 * 获得绘制文本的宽和高
    		 */
    		mPaint = new Paint();
    		mPaint.setTextSize(mTitleTextSize);
    		// mPaint.setColor(mTitleTextColor);
    		mBound = new Rect();
    		mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);
    
    	}

    我们重写了3个构造方法,默认的布局文件调用的是两个参数的构造方法,所以记得让所有的构造调用我们的三个参数的构造,我们在三个参数的构造中获得自定义属性。

    3、我们重写onDraw,onMesure调用系统提供的:

    @Override
    	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    	{
    		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    	}
    
    	@Override
    	protected void onDraw(Canvas canvas)
    	{
    		mPaint.setColor(Color.YELLOW);
    		canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
    
    		mPaint.setColor(mTitleTextColor);
    		canvas.drawText(mTitleText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
    	}
    此时的效果是:

    是不是觉得还不错,基本已经实现了自定义View。但是此时如果我们把布局文件的宽和高写成wrap_content,会发现效果并不是我们的预期:


    系统帮我们测量的高度和宽度都是MATCH_PARNET,当我们设置明确的宽度和高度时,系统帮我们测量的结果就是我们设置的结果,当我们设置为WRAP_CONTENT,或者MATCH_PARENT系统帮我们测量的结果就是MATCH_PARENT的长度。

    所以,当设置了WRAP_CONTENT时,我们需要自己进行测量,即重写onMesure方法”:

    重写之前先了解MeasureSpec的specMode,一共三种类型:

    EXACTLY:一般是设置了明确的值或者是MATCH_PARENT

    AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT

    UNSPECIFIED:表示子布局想要多大就多大,很少使用

    下面是我们重写onMeasure代码:

    	@Override
    	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    	{
    		int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    		int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    		int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    		int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    		int width;
    		int height ;
    		if (widthMode == MeasureSpec.EXACTLY)
    		{
    			width = widthSize;
    		} else
    		{
    			mPaint.setTextSize(mTitleTextSize);
    			mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds);
    			float textWidth = mBounds.width();
    			int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());
    			width = desired;
    		}
    
    		if (heightMode == MeasureSpec.EXACTLY)
    		{
    			height = heightSize;
    		} else
    		{
    			mPaint.setTextSize(mTitleTextSize);
    			mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds);
    			float textHeight = mBounds.height();
    			int desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());
    			height = desired;
    		}
    		
    		
    
    		setMeasuredDimension(width, height);
    	}
    

    现在我们修改下布局文件:

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:custom="http://schemas.android.com/apk/res/com.example.customview01"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    
        <com.example.customview01.view.CustomTitleView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            custom:titleText="3712"
            android:padding="10dp"
            custom:titleTextColor="#ff0000"
            android:layout_centerInParent="true"
            custom:titleTextSize="40sp" />
    
    </RelativeLayout>

    现在的效果是:


    完全复合我们的预期,现在我们可以对高度、宽度进行随便的设置了,基本可以满足我们的需求。

    当然了,这样下来我们这个自定义View与TextView相比岂不是没什么优势,所有我们觉得给自定义View添加一个事件:

    在构造中添加:

    this.setOnClickListener(new OnClickListener()
    		{
    
    			@Override
    			public void onClick(View v)
    			{
    				mTitleText = randomText();
    				postInvalidate();
    			}
    
    		});

    private String randomText()
    	{
    		Random random = new Random();
    		Set<Integer> set = new HashSet<Integer>();
    		while (set.size() < 4)
    		{
    			int randomInt = random.nextInt(10);
    			set.add(randomInt);
    		}
    		StringBuffer sb = new StringBuffer();
    		for (Integer i : set)
    		{
    			sb.append("" + i);
    		}
    
    		return sb.toString();
    	}

    下面再来运行:


    我们添加了一个点击事件,每次让它随机生成一个4位的随机数,有兴趣的可以在onDraw中添加一点噪点,然后改写为验证码,是不是感觉很不错。


    好了,各位学习的,打酱油的留个言,顶个呗~


    源码点击此处下载






    展开全文
  • Android --- View.inflate()的详细介绍

    千次阅读 多人点赞 2020-05-01 09:18:09
    误用 LayoutInflater 的 inflate() 方法已经不是什么稀罕...1.View.inflate() 和 LayoutInflator.from().inflate() 有啥区别? 2.调用 inflate() 方法的时候有时候传 null,有时候传 parent 是为啥? 3.用 LayoutInf...
  • 是时候拥抱ViewBinding了~

    万次阅读 多人点赞 2020-02-25 18:55:45
    一、前言二、初识ViewBinding三、拥抱ViewBinding3.1、环境要求3.2、开启ViewBinding功能3.3、Activity中ViewBinding的使用3.3.1、布局中直接的控件3.3.2、布局中导入的控件 沉舟侧畔千帆过, 病树前头万木春。 – ...
  • Qt之QGraphicsView进阶篇

    万次阅读 多人点赞 2020-04-06 15:50:35
    作者:billy 版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处 前言 上一章节介绍了 QGraphicsView 中的基础内容,具体请参考 Qt之QGraphicsView入门篇。这一章节我们来具体了解一下 ...
  • 真正的usbview源代码

    千次下载 热门讨论 2011-10-25 14:11:22
    真正的usbview源代码,带所需头文件及Lib,找了半天,在pudn中找到的,大家共享。 在vc6中link有错,不知为什么;在vs2008中编译通过。
  • Android视图绑定ViewBinding的使用

    万次阅读 2020-03-13 10:02:57
    后台读者留言:能否写一篇视图绑定ViewBinding相关的内容? 首先感谢这位读者的提议,让我抽出时间细看视图绑定的内容,也打算在项目中使用该功能。当然,还有其他读者提议的内容我已记录,后期有时间也会陆续更新。...
  • Qt之QGraphicsView入门篇

    万次阅读 多人点赞 2020-04-05 20:28:40
    从QT4.2开始引入了Graphics View框架用来取代QT3中的Canvas模块,并作出了改进,Graphics View框架实现了模型-视图结构的图形管理,能对大量图元进行管理,支持碰撞检测,坐标变换和图元组等多种方便的功能。...
  • 基于ViewBinding的BaseActivity封装尝试

    万次阅读 热门讨论 2020-03-09 22:09:26
    本文目录 1 新建项目并启用ViewBinding 2 普通的BaseActivity 3 基于ViewBinding的BaseActivity 4 解析ViewBinding实现思路 5 牺牲安全性的更方便的ViewBinding 2020.03.20修改 2020.01.26修改 软件环境 Android ...
  • QGraphicsView使用详解

    万次阅读 多人点赞 2019-04-15 17:59:03
    一、GraphicsView框架简介 QT4.2开始引入了Graphics View框架用来取代QT3中的Canvas模块,并作出了改进,Graphics View框架实现了模型-视图结构的图形管理,能对大量图元进行管理,支持碰撞检测,坐标变换和图元组...
  • json-view-chrome插件 JSONView 0.0.32.2官方绿色版

    千次下载 热门讨论 2015-06-05 14:51:42
    JSONView 0.0.32.2 从谷歌商店下载。 chrome://extensions/ 开发者模式 载入 或者Canary及Dev版本,拖入crx文件
  • Android View的绘制流程

    万次阅读 多人点赞 2018-03-29 19:51:21
    本文主要是梳理 View 绘制的整体流程,帮助开发者对 View 的绘制有一个更深层次的理解。整体流程View 绘制中主要流程分为measure,layout, draw 三个阶段。measure :根据父 view 传递的 MeasureSpec 进行计算大小...
  • VMware-viewclient-x86_64-5.3.0-1042023

    千次下载 热门讨论 2013-08-15 15:15:59
    VMware-viewclient 5.3的,64位操作系统的,现在64为操作系统的操作系统多了,所以。。。
  • SAP CDS View基础语法(创建你的第一个CDS View

    万次阅读 多人点赞 2020-04-30 20:52:57
    很多同学对于CDS view感觉无从下手,本篇博客将介绍CDS View的基础语法,并附有示例。 1. 定义一个CDS View 用途: 创建一个CDS View 语法: @AbapCatalog.sqlViewName: 'ZDEMO_CDS_SQL' define view ZDEMO_...
  • Android 自定义View (三) 圆环交替 等待效果

    千次下载 热门讨论 2014-04-25 23:32:40
    例子为博客的示例教程:http://blog.csdn.net/lmj623565791/article/details/24500107 有问题,博客留言
  • JsonView工具

    千次下载 热门讨论 2013-12-01 22:14:34
    JsonView文件,可以使用这个工具直接查看从浏览器返回的Json字符串,可以独立使用。建议查看服务器返回的数据使用Chrome或者火狐浏览器。
  • 近期pm提出需要统计首页商品的曝光亮,由于我们的首页是用的recylerview实现的,这里就来讲下如何使用监听recylerview的滚动事件来实现子view的曝光量统计,我们这里说的view都是列表中的子item条目(子view) ...
  • Android6.0源码分析之View(一)

    万次阅读 2016-12-19 14:33:27
    目前对于view还处于学习阶段,本来打算学习结束之后再写一篇进行总结,但是发现自己自制力太差,学习效率太低,所以在此,边学边写博客,不仅督促自己完成对view的学习,而且还可以看看大家对于view有什么想知道的,...
  • 作为国内最早一批的 Vue.js 组件库,View UI 来到了它的第 6 年。这 6 年中,我们从开源逐步向商业化探索,沉淀技术、丰富生态、积累用户。今天,我们正式发布新品牌 View Design。
  • 在布局过程中,我们需要一些容器具备可滑动的能力,尽管我们可以通过给<view/>设置overflow:scroll属性来实现,但由于小程序实现原理中没有DOM概念,我们没法直接监听<view/>滚动、触顶、触底等事件,这时便需要使用...
  • 初探ViewBinding

    万次阅读 2019-10-27 16:02:27
    视图访问的方式有常用的findViewById,ButterKnife等多种方式,这些...谷歌在Android Studio 3.6 Canary 11版本中正式推出视图绑定(View Binding),来看下使用方法, 首先需要使用AS 3.6 Canary 11之上的版本,这...
  • Django的视图View详解

    千次阅读 2019-05-29 17:48:41
    django的view可以是方法,也可以是类,按照django的规则,我们添加的view都要写到app的views.py文件中 其中,方法view我们称之为FBV(function base views),类view我们称之为CBV(class base views) 一般情况下...
  • python pytorch中 .view()函数讲解

    千次阅读 多人点赞 2021-12-08 10:13:04
    view()相当于reshape、resize,重新调整Tensor的形状。 import torch a1 = torch.arange(0,16) print(a1) tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) a2 = a1.view(8, 2) a3 = a1.view(2, ...
  • Android如何计算View的深度

    万次阅读 2019-01-22 17:22:56
    今天在QQ群上,看到大家谈到了一个面试题,就是如何求View树的深度。在我们项目中基本上比较少需要到这个计算,所以可能一下子会蒙圈了。 我们知道,Android的视图是一颗树的形式,那么即使关于Android的View树方面...
  • Qt使用QGraphicsView实现滑动窗体效果

    千次下载 热门讨论 2010-11-04 16:20:02
    QGraphicsView用来显示一个滚动视图区的QGraphicsScene内容。QGraphicsScene提供了QGraphicsItem的容器功能。通常与QGraphicsView一起使用来描述可视化图形项目。 QGraphicsScene提供了一个视图的场景,通过在这样...
  • vue router-view使用详解

    万次阅读 多人点赞 2021-08-14 15:08:16
      router-view组件作为vue最核心的路由管理组件,在项目中作为路由管理经常被使用到。vue项目最核心的App.vue文件中即是通过router-view进行路由管理。 <template> <div id="app"> <router-view&...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,856,753
精华内容 1,542,701
关键字:

view