首页 文章 精选 留言 我的

精选列表

搜索[网站开发],共10000篇文章
优秀的个人博客,低调大师

为 Web 开发者准备的 UI 设计工具和资源

Lumzy Fillerati Fivesecondtest GuideGuide MockFlow Adobe Kuler Android Asset Studio DIY Kit The White Stripes UI Kit Gliffy Flow – Mobile App Landing Page Template User Interface Design Framework The Ero Widget UI Kit iPhone Mockup Eclipse Stencil for OmniGraffle OSX Leopard GUI Set DHTMLX LivePipe UI Small GUI Pack Android UI Elements Set The Pencil Project iPlotz Editable Grid Scripty 2 ZKoss Dojo DesignerVista Napkee Echo 3 Go Mockingbird via inspiretrends 文章转载自 开源中国社区 [http://www.oschina.net]

优秀的个人博客,低调大师

Android开发艺术探索——第八章:理解Window和WindowManager

理解Window和WindowManager Window表示的是一个窗口的概念,在日常生活中使用的并不是很多,但是某些特殊的需求还是需要的,比如悬浮窗之类的,他的具体实现是PhoneWindow,创建一个Window很简单,只需要WindowManager去实现,WindowManager是外界访问Window的入口,Window的具体实现是在WindowManagerService中,他们两个的交互是一个IPC的过程,Android中的所有视图都是通过Windowl来实现的,无论是Activity,Dialog还是Toast,他们的视图都是直接附加在Window上的,因此Window是View的直接管理者,在之前的事件分发中我们说到,View的事件是通过WIndow传递给DecorView,然后DecorView传递给我们的View,就连Activity的setContentView,都是由Window传递的。 一.Window和WindowManager 为了了解Window的工作机制,我们首先来看下如何通过WindowManager来创建一个Window Button btn = new Button(this); btn.setText("我是窗口"); WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE); WindowManager.LayoutParams layout = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT , WindowManager.LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSLUCENT); layout.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; layout.gravity = Gravity.CENTER; layout.type = WindowManager.LayoutParams.TYPE_PHONE; layout.x = 300; layout.y = 100; wm.addView(btn, layout); 上述的代码,其中type和flag是比较重要的,我们来看下 Flag参数表示window的属性,他有很多选项,我们挑几个重点的 FLAG_NOT_FOCUSABLE 表示窗口不需要获取焦点,也不需要接收各种事件,这属性会同时启动FLAG_NOT_TOUCH_MODAL,最终的事件会传递给下层的具体焦点的window FLAG_NOT_TOUCH_MODAL, 在此模式下,系统会将当前window区域以外的单击事件传递给底层的Window,此前的Window区域以内的单机事件自己处理,这个标记很重要,一般来说都需要开启,否则其他window将无法获取单击事件 FLAG_SHOW_WHEN_LOCKED 开启这个属性可以让window显示在锁屏上 Type参数表示window的类型,window有三种类型,分别是应用,子,系统,应用window对应一个Activity,子Window不能单独存在,需要依赖一个父Window,比如常见的Dialog都是子Window,系统window需要声明权限,比如系统的状态栏 Window是分层的,每个Window对应着z-ordered,层级大的会覆盖在层级小的Window上面,这和HTML中的z-index的概念是一致的,在这三类中,应用是层级范围是1-99,子window的层级是1000-1999,系统的层级是2000-2999。这些范围对应着type参数,如果想要window在最顶层,那么层级范围设置大一点就好了,很显然系统的值要大一些,系统的值很多,我们一般会选择TYPE_SYSTEM_OVERLAY和TYPE_SYSTEM_ERROR,记得要设置权限哦; <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> WindowManager所提供的功能很简单,常用的有三个方法,添加View,更新View,删除View,这三个方法定义在ViewManager中,而WindowManager继承自ViewManager public interface ViewManager { public void addView(View view, ViewGroup.LayoutParams params); public void updateViewLayout(View view, ViewGroup.LayoutParams params); public void removeView(View view); } 虽然只有三个功能,但是这三个功能足够我们使用了,我们常见的可以推动的View,其实也很好实现,就是不断的更改他xy的位置 btn.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { int rawX = (int) event.getRawX(); int rawY = (int) event.getRawY(); switch (event.getAction()) { case MotionEvent.ACTION_MOVE: layout.x = rawX; layout.y = rawY; wm.updateViewLayout(btn,layout); break; } return false; } }); 二.Window的内部机制 Window是一个抽象的概念,每一个Window对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl建立关系,因此Window并不是实际存在的,这点从WindowManager定义的接口都是针对View,这说明View才是window的实体,在实际使用当中我们并不能直接访问 1.Window的添加过程 Window的添加过程是通过WindowManager的addView去实现的,而真正实现的是一个接口,,也就是WindowManagerImpl @Override public void addView(View view, ViewGroup.LayoutParams params) { mGlobal.addView(view, params, mDisplay, mParentWindow); } @Override public void updateViewLayout(View view, ViewGroup.LayoutParams params) { mGlobal.updateViewLayout(view, params); } @Override public void removeView(View view) { mGlobal.removeView(view, false); } 可以发现,WindowManagerImpl并没有直接去实现一个Window的三大操作,而是全部交给了WindowManagerGlobal来处理,WindowManagerGlobal是一个工厂的性质提供自己的实现, 在WindowManagerGlobal中有一段如下的代码: private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance(); WindowManagerImpl这种工作模式就是典型的桥接模式,将所有的操作全部委托给WindowManagerGlobal去实现,WindowManagerGlobal的addView方法主要分如下几步: 1.检查参数是否合法,如果是子Window还需要调整一下参数 if (view == null) { throw new IllegalArgumentException("view must not be null"); } if (display == null) { throw new IllegalArgumentException("display must not be null"); } if (!(params instanceof WindowManager.LayoutParams)) { throw new IllegalArgumentException("Params must be WindowManager.LayoutParams"); } final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params; if (parentWindow != null) { parentWindow.adjustLayoutParamsForSubWindow(wparams); } 2.创建ViewRootImpl并将View添加到列表中 在WindowManagerGlobal有如下几个列表是比较重要的 private final ArrayList<View> mViews = new ArrayList<View>(); private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>(); private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>(); private final ArraySet<View> mDyingViews = new ArraySet<View>(); 在上面的声明中,mViews存储所有window所对应的View,mRoots存储是所有window所对应的ViewRootImpl,mParams存储是所对应的布局参数 ,而mDyingViews则存储那些正在被删除的对象,在addView中通过如下方式将Window的一系列对象添加到列表中 root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); mRoots.add(root); mParams.add(wparams); 3.通过ViewRootImpl来更新界面并完成Window的添加 这个步骤由ViewRootImpl的setView完成,他内部会通过requstLayout来万和城呢过异步刷新请求,在下面代码中,scheduleTraversals实际上就是View绘制的入口 @Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } } 接下来会通过WindowSession最终来完成Window的添加过程,在下面的代码中,WindowSession的类型IWindowSession,他是一个Binder对象,也是一次真正意义上的IPC操作 代码缺失 在Session内部会通过WindowManagerService来实现Window的添加,代码如下: 代码缺失 如此一来,Window的添加请求就交给WindowManagerService去处理了,在WindowManagerService内部为每一个应用添加了一个单独的Session,具体WIndow在WindowManagerService中怎么添加的,我们就不去分析了,读者可以自己去熟悉下 2.Window的删除过程 Window的删除过程和添加过程一样,都是通过Impl再通过Global来实现了,下载是Global的removeView 代码 public void removeView(View view, boolean immediate) { if (view == null) { throw new IllegalArgumentException("view must not be null"); } synchronized (mLock) { int index = findViewLocked(view, true); View curView = mRoots.get(index).getView(); removeViewLocked(index, immediate); if (curView == view) { return; } throw new IllegalStateException("Calling with view " + view + " but the ViewAncestor is attached to " + curView); } } removeView的代码很清晰,首先通过findViewLocked来查找待删除的索引,这个超找过程就是建立的数组遍历,然后调用removeViewLocked来进一步的删除 private void removeViewLocked(int index, boolean immediate) { ViewRootImpl root = mRoots.get(index); View view = root.getView(); if (view != null) { InputMethodManager imm = InputMethodManager.getInstance(); if (imm != null) { imm.windowDismissed(mViews.get(index).getWindowToken()); } } boolean deferred = root.die(immediate); if (view != null) { view.assignParent(null); if (deferred) { mDyingViews.add(view); } } } removeViewLocked是通过ViewRootImpl来完成删除操作的,在windowmanager中提供了两种接口removeView和removeViewImmediate,他们分别表示异步删除和同步删除,其中removeViewImmediate,使用起来要格外注意,一般来说不需要使用此方法来删除window以免发生意外的错误,这里主要是异步删除的问题,具体的删除操作是ViewImple的die方法来完成的,在异步删除的情况下,die只是发生一个删除的请求后就返回了,这个时候View并没有完成删除的操作,所有最后会将其添加到mDyingViews中,mDyingViews表示待删除的View列表,ViewRootImpe的die方法如下: boolean die(boolean immediate) { // Make sure we do execute immediately if we are in the middle of a traversal or the damage // done by dispatchDetachedFromWindow will cause havoc on return. if (immediate && !mIsInTraversal) { doDie(); return false; } if (!mIsDrawing) { destroyHardwareRenderer(); } else { Log.e(TAG, "Attempting to destroy the window while drawing!\n" + " window=" + this + ", title=" + mWindowAttributes.getTitle()); } mHandler.sendEmptyMessage(MSG_DIE); return true; } 在die方法内部只是做了简单的判断,那么就发送了一个MSG_DIE的消息,ViewRootImpl中的mHandler会处理此消息并且并调用doDie方法,如果是同步删除就会直接调用doDie方法,在doDie方法内部会操作dispatchDetachedFromWindow,真正删除window就是在这里面实现的,他主要做了四件事 1.垃圾回收相关的工作,比如清除数据和消息,移除回调 2.通过Session的remove方法来删除window,这同样是一个IPC的过程,最终会调用wms的removeWindow方法 3.调用view的dispatchDetachedFromWindow 方法,在内不会调用onDetachedFromWindow,他做了一些回收资源或者停止动画的一些操作 4.调用WindowManagerGlobal的doRemoveView方法刷新数据 3.Window的更新过程 到这里,window的删除就接收完了,在说下更新,需要看WindowManagerGlobal的updateViewLayout方法 public void updateViewLayout(View view, ViewGroup.LayoutParams params) { if (view == null) { throw new IllegalArgumentException("view must not be null"); } if (!(params instanceof WindowManager.LayoutParams)) { throw new IllegalArgumentException("Params must be WindowManager.LayoutParams"); } final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params; view.setLayoutParams(wparams); synchronized (mLock) { int index = findViewLocked(view, true); ViewRootImpl root = mRoots.get(index); mParams.remove(index); mParams.add(index, wparams); root.setLayoutParams(wparams, false); } } updateViewLayout做的方法比较简单,首先他更新View的LayoutParams替换老的,接着再更新下ViewRootimpl中的LayoutParams,这一步是通过viewrootimpl的setLayoutParams来做的,在ViewRootImpl中会通过scheduleTraversals方法来对View,测量,布局,重绘等等,除了view本身的重绘之外,ViewRootImpl还会通过WindowSession来更新Window的视图,这个过程最终是WindowManagerService的relayoutWindow来实现的,具体也是一个IPC的过程 三.Window的创建过程 通过上面的分析,我们知道,view是Android中视图的呈现方式,但是view不能单独存在,他必须依附在window这个抽象类中,因此有视图的地方就有window,activity,toast都是,我们继续来分析window的创建过程 1.Activity的Window创建过程 要分析Activity的Window创建过程就需要去了解activity的启动过程,详细的会在后面说,这里简单概括,activity的启动过程很复杂,最终会由ActivityThread中的perfromLaunchActivity()来完成整个启动过程 if (activity != null) { Context appContext = createBaseContextForActivity(r, activity); CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager()); Configuration config = new Configuration(mCompatConfiguration); if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity " + r.activityInfo.name + " with config " + config); activity.attach(appContext, this, getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, r.parent, r.embeddedID, r.lastNonConfigurationInstances, config, r.voiceInteractor); 在Activity的attach方法中,系统会创建activity所属的window对象并为其设置回调接口,window对象的创建过程是由PolicyManager的akeNewWindow方法实现的,由于activity实现了window的callback方法接口,因此当window接受到外界的状态改变的时候就会去调用activity的方法,callback接口中的方法很多,但是有几个确实我们非常熟悉的,你如onAttachedToWindow等 代码缺失 从上面的分析来看,activity会通过window是通过PolicyManager的一个工厂方法来创建的,但是从PolicyManager的类名来看,他不是一个普通的类,他是一个chelv类,PolicyManager中实现的几个工厂方法全部在接口IPolicy中声明,我们来看下这个接口: public interface IPolicy { public Window makeNewWindow(Context context); public LayoutInflater makeNewLayoutInflater(Context context); public WindowManagerPolicy makeNewWindowManager(); public FallbackEventHandler makeNewFallbackEventHandler(Context context); } 在实际的调用中,PolicyManager是如何关联到Policy类中的mackNewWindow方法来实现如下,由此可以发现,window的具体实现的确就是phoneWindow public static Window makeNewWindow(Context context) { return new PhoneWindow(context); } 关于PolicyManager是如何关联到IPolicy中,这个无法从源码中调用关系得到,这里的猜测可能是编译环节动态控制的,到这里window已经创建完成了,下面分析activity的视图是怎么依附在window上的由于activity的视图是由setContentView开始的,所有我们先看下这个方法: public void setContentView(int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); } 这里可大致的看出, 1.如果没有DecorView就去创建他 DecorView是一个FrameLayout,在之前就已经说过了,这里说一下。DecorView是activity中的顶级View,一般来说他的内部包含标题栏和内部栏,但是这个会随着主题的变化而发生改变的,不管怎么样,内容是一定要存在的,并且内容有固定的id,那就是content,完整的就是android.R.id.content,DecorView的创建是由installDecor方法来完成的,在方法内部会通过generateDecor方法来完成创建DecorView,这个时候他就是一个空白的FrameLayout protected DecorView generateDecor(){ return new DecorView(getContext(),-1); } 为了初始化DecorView的结构,PhoneWindow还需要通过generateLayout方法来加载具体的布局文件到DecorView中,这个跟主题有关: View in = mLayoutInflater.inflate(layoutResource, null); decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); mContentRoot = (ViewGroup) in; ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); 其中ID_ANDROID_CONTENT的定义如下,这个id对应的就是ViewGroup的mContentParent public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content; 2.将View添加到DecorView的mContentParent中 这个过程比较简单,由于在第一步的时候已经初始化了DecorView,因此这一部就直接将activity的视图添加到DecorView的mContentParent中既可,mLayoutInflater.inflate(layoutResID, mContentParent);到此为止,由此可以理解activity的setcontentview的来历了,也许有读者会怀疑,为什么不叫setview来,他明明是给activity设置视图啊,从这里来看,他的确不适合叫做setview,因为activity的布局文件只是添加到了DecorView的mContentParent中,因此交setContentView更加准确。 3.回调Activity的onCreateChanged方法来通知Activity视图已经发生改变 这个过程很简单,由于window实现了Callback接口,这里表示布局已经被添加到DecorView的mContentParent中了,于是通知activity。使其可以做相应的处理Activity的onCreateChanged是一个空实现,我们可以在子activity处理这个回调 final callback cb = getCallback(); if(cb != null && !isDestroyed()){ cb.onContentChanged(); } 经过了上面的三个步骤,到这里为止DecorView已经被创建并且初始化完毕了activity的布局文件也已经添加到了DecorView的内容中,但是这个时候DecorView还没有被windowmanager添加到window中,这里需要正确的理解window的概念,window更多的是表示一种抽象的功能集合,虽然说早在activity的attch中window就已经被创建了,但是这个时候由于DecorView还没有被windowmanager识别,所有还不能提供具体的功能,因为他还无法接收外界的输入,在activityThread的makeVisible中,才能被视图看到: void makeVisible() { if (!mWindowAdded) { ViewManager wm = getWindowManager(); wm.addView(mDecor, getWindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(View.VISIBLE); } 到这里,window的创建过程就已经分析完了 2.Dialog的Window创建过程 有如下的几个步骤 1.创建Window dialog的window创建过程和activity的类似,我们来看下 Dialog(Context context, int theme, boolean createContextThemeWrapper) { if (createContextThemeWrapper) { if (theme == 0) { TypedValue outValue = new TypedValue(); context.getTheme().resolveAttribute(com.android.internal.R.attr.dialogTheme, outValue, true); theme = outValue.resourceId; } mContext = new ContextThemeWrapper(context, theme); } else { mContext = context; } mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); Window w = PolicyManager.makeNewWindow(mContext); mWindow = w; w.setCallback(this); w.setOnWindowDismissedCallback(this); w.setWindowManager(mWindowManager, null, null); w.setGravity(Gravity.CENTER); mListenersHandler = new ListenersHandler(this); } 2.初始化DecorView并将Dialog的师徒添加到DecorView 这个过程也和activity的类似,都是通过window去指定加载的布局文件 public void setContentView(int layoutResID) { mWindow.setContentView(layoutResID); } 3.将DecorView添加到window并且显示 在Dialog的show方法中,会通过windowmanager将DecorView添加到window中 mWindowManager.addView(mDecor, l); mShowing = true; 从上述的三个步骤可以发现,dialog的创建过程和activity很类似 普通的dialog有一个特殊的地方,那就是必须用activity的context,否则会报错 Dialog dialog = new Dialog(this); dialog.setTitle("Hello"); dialog.show(); 上面的信息非常的明确,是没有应用token导致的,而应用token一般只有activity持有,所有这里需要activity的context,另外系统比较特殊,他可以不需要token 3.Toast的Window创建过程 Toast和dialog不同,他的工作过稍微复杂一点,首先Toast也是基于Window来实现的,但是由于Toast具有定时取消的功能,所以系统采用了handler,在toast内部有两类IPC的过程,第一类Toast访问NotificationManagerService,第二类是NotificationManagerService回调toast的TN接口 Toast具有系统的window,他内部试下的师徒有两种方式制定,一个是系统设定,还可以setview指定 /** * Show the view for the specified duration. */ public void show() { if (mNextView == null) { throw new RuntimeException("setView must have been called"); } INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; try { service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty } } /** * Close the view if it's showing, or don't show it if it isn't showing yet. * You do not normally have to call this. Normally view will disappear on its own * after the appropriate duration. */ public void cancel() { mTN.hide(); try { getService().cancelToast(mContext.getPackageName(), mTN); } catch (RemoteException e) { // Empty } } 从上面的代码可以看出,显示和隐藏是通过NMS来实现的,由于NMS运行在系统,所以只能通过远程调用,这里就跨进程实现了IPC,在这个时候NMS是运行在binder线程池中,所以需要handler切换到主线程中,所以这就意味着Toast无法再没有Lopper的线程中弹出 我们首先来看下Toast的显示过程 INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; try { service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty } NMS的enqueueToast方法的第一个参数就是当前应用的包名,第二个参数表示远程回调,第三个是时长,enqueueToast首先将Toast请求封装为一个ToastRecord对象将其添加到一个队列中,其实这本书就是一个arraylist,对于非系统应用来说,他只能存50个,这样是为了防止dos,如果不这样做,试想一下,其他应用还能弹出东西来吗? 代码丢失 正常情况下,一个应用不可能达到上限,当ToastRecord被添加到mToastQueue中后,NMS就会通过showNextToastLocked方法来显示Toast,下面的代码很好理解,需要注意的是,Toast的显示是由ToastRecord的callback来完成的,这个callback实际上就是TN对象的远程Binder,通过callback来访问TN中的方法是需要跨进程来完成的,最终被调用的对象是TN中方法发起在Binder线程池中。 void showNextToastLocked() { ToastRecord record = mToastQueue.get(0); while (record != null) { if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback); try { record.callback.show(record.token); scheduleTimeoutLocked(record); return; } catch (RemoteException e) { Slog.w(TAG, "Object died trying to show notification " + record.callback + " in package " + record.pkg); // remove it from the list and let the process die int index = mToastQueue.indexOf(record); if (index >= 0) { mToastQueue.remove(index); } keepProcessAliveIfNeededLocked(record.pid); if (mToastQueue.size() > 0) { record = mToastQueue.get(0); } else { record = null; } } } } Toast显示出来之后,NMS会通过scheduleTimeoutLocked来发送一个延时消息 private void scheduleTimeoutLocked(ToastRecord r) { mHandler.removeCallbacksAndMessages(r); Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r); long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY; mHandler.sendMessageDelayed(m, delay); } 上面的代码,LONG_DELAY是3.5s,SHORT_DELAY是2s,NMS会通过cancelToastLocked来隐藏toast并且清楚队列 ToastRecord record = mToastQueue.get(index); try { record.callback.hide(); } catch (RemoteException e) { Slog.w(TAG, "Object died trying to hide notification " + record.callback + " in package " + record.pkg); // don't worry about this, we're about to remove it from // the list anyway } 通过上面的分析,大家知道toast的显示隐藏实际上是toast的TN这个类来实现的,分别对应的show/hide,由于这两个方法都是NMS以跨进程的方式调用的,因此他运行在Binder池中,为了切换,在他们内部使用了handler /** * schedule handleShow into the right thread */ @Override public void show(IBinder windowToken) { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.obtainMessage(0, windowToken).sendToTarget(); } /** * schedule handleHide into the right thread */ @Override public void hide() { if (localLOGV) Log.v(TAG, "HIDE: " + this); mHandler.post(mHide); } 上述的代码,mShow和mHide是两个Runnable,他们的内部实现分别调用了具体方法,由此可见,handlershow才是真正的方法, public void handleShow(IBinder windowToken) { if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView + " mNextView=" + mNextView); if (mView != mNextView) { // remove the old view if necessary handleHide(); mView = mNextView; Context context = mView.getContext().getApplicationContext(); String packageName = mView.getContext().getOpPackageName(); if (context == null) { context = mView.getContext(); } mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); // We can resolve the Gravity here by using the Locale for getting // the layout direction final Configuration config = mView.getContext().getResources().getConfiguration(); final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection()); mParams.gravity = gravity; if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { mParams.horizontalWeight = 1.0f; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { mParams.verticalWeight = 1.0f; } mParams.x = mX; mParams.y = mY; mParams.verticalMargin = mVerticalMargin; mParams.horizontalMargin = mHorizontalMargin; mParams.packageName = packageName; mParams.hideTimeoutMilliseconds = mDuration == Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT; mParams.token = windowToken; if (mView.getParent() != null) { if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this); mWM.removeView(mView); } if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this); mWM.addView(mView, mParams); trySendAccessibilityEvent(); } } 而移除 public void handleHide() { if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView); if (mView != null) { // note: checking parent() just to make sure the view has // been added... i have seen cases where we get here when // the view isn't yet added, so let's try not to crash. if (mView.getParent() != null) { if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this); mWM.removeViewImmediate(mView); } mView = null; } } } 到这里,我们的window就全部分析完成了,相信大家对window有了一些新的见解了 好的,本章没有什么例子,代码全在上面 来我的群玩玩吧 1.通往Android的神奇之旅 555974449 (快满了) 2.Android旅行的路途 484167109 3.Android进阶深度学习群 515171658(此群精品付费) 也可以关注我的微信公众号

优秀的个人博客,低调大师

Android开发技巧——实现设计师给出的视觉居中的布局

本篇主要是对自定义控件的测量方法(onMeasure(int widthMeasureSpec, int heightMeasureSpec))在实际场景中的运用。 在移动应用的设计中,经常有这样的界面:某个界面的元素非常少,比如空列表界面,或者某某操作成功的界面,只有一两个元素在中间。但是它们在某个布局里又不是数学上的那个居中,而是经过设计师调出来的“视觉居中”。这种“视觉居中”内部是怎么计算的,我大致也不懂,反正结果就是设计师们看起来要显示的信息给人有感觉是在中间的(通常是比中间偏上一点)。 既是这样,那我们在布局中就不能用gravity="center"或layout_gravity="center"等这样的属性来设置了。而使用固定的padding或margin来调整它的位置,也难以在不同的屏幕分辨率中实现同样的效果,那就只好按钮设计图的标注,按比例来计算它的位置了。 按比例来调整子view与layout中的距离,在约束布局(ConstraintLayout)中是可以做到的,但是在我个人看来相对这样简单的需求,约束布局有点重了,并且它的依赖在不同方式的编译下总是很容易出问题(比如在自己电脑编译通过,在travis-ci上编译就提示找不到该库的某个版本),还有拖拽生成的代码格式不是很整齐(我有代码洁癖),总需要自己再去格式化一下代码。那么自定义实现一下也是可以的嘛。 首先像这样简单的界面,通常来说,使用LinearLayout就足够了。我们所需要的只是按比例计算出padding然后设进去,那么内容就能够按我们想要的位置去显示了。在这里,我把这个布局定义为PaddingWeightLinearLayout,即可以按权重来设计它的padding。它提供了四个属性: <declare-styleable name="PaddingWeightLinearLayout"> <attr name="ppLeftPadding" format="integer"/> <attr name="ppRightPadding" format="integer"/> <attr name="ppTopPadding" format="integer"/> <attr name="ppBottomPadding" format="integer"/> </declare-styleable> 分别代表每个方向上的padding的权重。 在构造方法里获取这些属性的值: private final int mTopWeight; private final int mBottomWeight; private final int mLeftWeight; private final int mRightWeight; public PaddingWeightLinearLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PaddingWeightLinearLayout); mTopWeight = ta.getInteger(R.styleable.PaddingWeightLinearLayout_ppTopPadding, 0); mBottomWeight = ta.getInteger(R.styleable.PaddingWeightLinearLayout_ppBottomPadding, 0); mLeftWeight = ta.getInteger(R.styleable.PaddingWeightLinearLayout_ppLeftPadding, 0); mRightWeight = ta.getInteger(R.styleable.PaddingWeightLinearLayout_ppRightPadding, 0); ta.recycle(); } 那么接下来,我们只需要计算出所有子View所占的空间,然后计算出水平和竖直方向上剩余的空间,按比例分给这四个padding就可以了。之所以使用LinearLayout是因为它是线性布局,子View按线性排列,比较利于计算。如下图(电脑的图画不好,献丑了): 图1表示的是水平方向的布局,那么它的内容所占的大小是: 宽度为所有子View的宽度加上其左右Margin的总和。 高度为子View高度加上其上下Margin中最高的一个。 图2 是竖直方向的布局,它的内容所占的大小是: 宽度为子View宽度加上其左右Margin中最大的一个。 高度为所有子View的高度加上其上下Margin的总和。 因此,我们要先测量出子View的大小,然后再进行计算。 测试是在onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法中进行的。其中参数分别表示父布局能够提供给它的空间。这个int类型的参数分为两部分,高2位表示的是模式(mode),后面的30位表示的是具体的大小。这里的mode一共有三个: UNSPECIFIED 父View对子View没有任何约束,可以随意指定大小 EXACTLY 父View给子View一个固定的大小,子View会被这些边界限制,不管它自己想要多大 AT_MOST 子视图的大小可以是自己想要的值,但是不能超过指定的值 当我们要计算子View时,我们需要调用子View的measure(widthMeasureSpec, int heightMeasureSpec)方法,为了能够得到子View的确定大小,我们需要将widthMeasureSpec mode指定为AT_MOST,代码如下(以下代码都是在onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法内): int layoutWidth = MeasureSpec.getSize(widthMeasureSpec); int layoutHeight = MeasureSpec.getSize(heightMeasureSpec); int widthSpec = MeasureSpec.makeMeasureSpec(widthMeasureSpec, MeasureSpec.AT_MOST); int heightSpec = MeasureSpec.makeMeasureSpec(heightMeasureSpec, MeasureSpec.AT_MOST); 然后遍历所有子View,计算子View宽高的总和及最大值: for (int i = 0; i < childCount; i++) { View child = getChildAt(i); child.measure(widthSpec, heightSpec); LayoutParams params = (LayoutParams) child.getLayoutParams(); int width = child.getMeasuredWidth() + params.leftMargin + params.rightMargin; int height = child.getMeasuredHeight() + params.topMargin + params.bottomMargin; totalWidth += width; totalHeight += height; if (width > maxWidth) { maxWidth = width; } if (height > maxHeight) { maxHeight = height; } } 然后计算出在水平及竖直方向上剩余的空间: int spaceHorizontal; int spaceVertical; if (getOrientation() == VERTICAL) { spaceHorizontal = layoutWidth - maxWidth; spaceVertical = layoutHeight - totalHeight; } else { spaceHorizontal = layoutWidth - totalWidth; spaceVertical = layoutHeight - maxHeight; } if (spaceHorizontal < 0) { spaceHorizontal = 0; } if (spaceVertical < 0) { spaceVertical = 0; } 最后算出各个方向的padding,设置进去,然后重新调用父类的onMeasure(int, int)方法: int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingRight(); int paddingTop = getPaddingTop(); int paddingBottom = getPaddingBottom(); int horizontalWeight = mLeftWeight + mRightWeight; if (spaceHorizontal > 0 && horizontalWeight > 0) { paddingLeft = mLeftWeight * spaceHorizontal / horizontalWeight; paddingRight = spaceHorizontal - paddingLeft; } int verticalWeight = mTopWeight + mBottomWeight; if (spaceVertical > 0 && verticalWeight > 0) { paddingTop = mTopWeight * spaceVertical / verticalWeight; paddingBottom = spaceVertical - paddingTop; } setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); super.onMeasure(widthMeasureSpec, heightMeasureSpec); 下面我们只需要在写布局代码的时候,按照设计图填入标注的padding值,就可以按比例计算出内边距并设置,从而让我们的内容按照比例位置显示了: <com.parkingwang.widget.PaddingWeightLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white" android:orientation="vertical" android:paddingLeft="@dimen/screen_padding" android:paddingRight="@dimen/screen_padding" app:ppBottomPadding="287" app:ppTopPadding="85"> ... 内容布局代码 </com.parkingwang.widget.PaddingWeightLinearLayout> 下面分別是实现的效果以及设计图,可以看到,在内容区域它们所在的位置是相同的(由于屏幕分辨率的关系,大小会有微小差异)。 完整代码请参见Github项目:https://github.com/msdx/androidsnippet

资源下载

更多资源
Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册