美文网首页移动开发狂热者(299402133)Launcher源码详解Launcher
墨香带你学Launcher之(七)--小部件的加载、添加以及大小

墨香带你学Launcher之(七)--小部件的加载、添加以及大小

作者: 翰墨飘香 | 来源:发表于2016-12-22 20:45 被阅读774次

    上一章墨香带你学Launcher之(六)--拖拽我们介绍了Launcher的拖拽过程,涉及到的范围比较广,包括图标的拖拽,桌面上CellLayout的拖拽,小部件的拖拽,以及跨不同部件的拖拽,设计思想非常巧妙,不过整个流程相对也比较好掌握,只要跟着上一章的流程自己多跟踪几遍基本就熟悉了。按照计划本章我们继续学习Launcher的Widget的加载、添加以及Widget的大小调节。

    Widget的数据加载

    其实我们在第二章墨香带你学Launcher之(二)-数据加载流程介绍过Widget数据的加载,相对只是简单的做了介绍,下面我们稍微讲的详细点。

    我们知道Widget的数据加载开始在LauncherModel中的updateWidgetsModel方法中,我们看下代码:

        void updateWidgetsModel(boolean refresh) {
            PackageManager packageManager = mApp.getContext().getPackageManager();
            final ArrayList<Object> widgetsAndShortcuts = new ArrayList<Object>();
            widgetsAndShortcuts.addAll(getWidgetProviders(mApp.getContext(), refresh));
            Intent shortcutsIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT);
            widgetsAndShortcuts.addAll(packageManager.queryIntentActivities(shortcutsIntent, 0));
            mBgWidgetsModel.setWidgetsAndShortcuts(widgetsAndShortcuts);
        }
    

    上面代码我们可以看到是通过调用getWidgetProviders(mApp.getContext(), refresh)方法来获取所有Widget的,代码:

    public static List<LauncherAppWidgetProviderInfo> getWidgetProviders(Context context,
                                                                             boolean refresh) {
            ArrayList<LauncherAppWidgetProviderInfo> results =
                    new ArrayList<LauncherAppWidgetProviderInfo>();
            try {
                synchronized (sBgLock) {
                    if (sBgWidgetProviders == null || refresh) {
                        HashMap<ComponentKey, LauncherAppWidgetProviderInfo> tmpWidgetProviders
                                = new HashMap<>();
                        AppWidgetManagerCompat wm = AppWidgetManagerCompat.getInstance(context);
                        LauncherAppWidgetProviderInfo info;
    
                        List<AppWidgetProviderInfo> widgets = wm.getAllProviders();
                        for (AppWidgetProviderInfo pInfo : widgets) {
                            info = LauncherAppWidgetProviderInfo.fromProviderInfo(context, pInfo);
                            UserHandleCompat user = wm.getUser(info);
                            tmpWidgetProviders.put(new ComponentKey(info.provider, user), info);
                        }
    
                        Collection<CustomAppWidget> customWidgets = Launcher.getCustomAppWidgets().values();
                        for (CustomAppWidget widget : customWidgets) {
                            info = new LauncherAppWidgetProviderInfo(context, widget);
                            UserHandleCompat user = wm.getUser(info);
                            tmpWidgetProviders.put(new ComponentKey(info.provider, user), info);
                        }
                        // Replace the global list at the very end, so that if there is an exception,
                        // previously loaded provider list is used.
                        sBgWidgetProviders = tmpWidgetProviders;
                    }
                    results.addAll(sBgWidgetProviders.values());
                    return results;
                }
            } catch (Exception e) {
                ...    
            }
        }
    

    我们看到首先是初始化AppWidgetManagerCompat,我们之前介绍过带有Compat的是兼容组件,我们看看是怎么兼容的,

    launcher01.png

    我们下面代码:

    public static AppWidgetManagerCompat getInstance(Context context) {
            synchronized (sInstanceLock) {
                if (sInstance == null) {
                    if (Utilities.ATLEAST_LOLLIPOP) {
                        sInstance = new AppWidgetManagerCompatVL(context.getApplicationContext());
                    } else {
                        sInstance = new AppWidgetManagerCompatV16(context.getApplicationContext());
                    }
                }
                return sInstance;
            }
        }
    

    我们可以看到AppWidgetManagerCompat的初始化有两个,一个是当Api版本高于21(包含21)时,用AppWidgetManagerCompatVL,低于21时用AppWidgetManagerCompatV16,这两个有什么不同,我们下面分析。

    下面我们看如何获取Widget列表对象:

    List<AppWidgetProviderInfo> widgets = wm.getAllProviders();
    

    getAllProviders()方法是一个抽象方法,所以我们看哪里进行了复写,

    launcher02.png

    可以看到还是上面两个兼容类复写了该方法,我们看这个两个类中做了什么处理,先看V16中的:

        @Override
        public List<AppWidgetProviderInfo> getAllProviders() {
            return mAppWidgetManager.getInstalledProviders();
        }
    

    我们再看mAppWidgetManager这个是在哪里初始化,

        @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
        @Override
        public boolean bindAppWidgetIdIfAllowed(int appWidgetId, AppWidgetProviderInfo info,
                Bundle options) {
            if (Utilities.ATLEAST_JB_MR1) {
                return mAppWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, info.provider, options);
            } else {
                return mAppWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, info.provider);
            }
        }
    

    里面有个if语句,我们可以看到当Api大于等于17时,调用第一个进行初始化,否则调用第二个方法进行初始化,这就是对不同手机版本做的兼容。在我们写App的时候如果遇到相似情况也可以这么处理。

    我们再看一下VL中的getAllProviders()方法:

        @Override
        public List<AppWidgetProviderInfo> getAllProviders() {
            ArrayList<AppWidgetProviderInfo> providers = new ArrayList<AppWidgetProviderInfo>();
            for (UserHandle user : mUserManager.getUserProfiles()) {
                providers.addAll(mAppWidgetManager.getInstalledProvidersForProfile(user));
            }
            return providers;
        }
    

    和V16中的不一样了,这里面是通过for循环来获取的,其中有个UserHandle,那么在源码中给出的解释是设备中的每个用户,个人理解应该是每个应用,每个应用会有0-N个Widget,也就是从每个应用中获取每个应用的Widget列表。这样for循环就可以获取整个手机中所有应用的widget列表了。

    再回到上面getWidgetProviders方法的代码中,我们接着看,接着for循环AppWidgetProviderInfo列表信息,重构LauncherAppWidgetProviderInfo对象,这里有点怪,为啥有了AppWidgetProviderInfo对象还要重构一个LauncherAppWidgetProviderInfo对象,我们知道在写插件的时候每个Widget都会有一个类继承AppWidgetProvider,这样才会有一个插件,因此我们知道AppWidgetProviderInfo对象肯定是AppWidgetProvider的对象,那么LauncherAppWidgetProviderInfo是什么,我们接着看能不能找到答案,LauncherAppWidgetProviderInfo的初始化时通过

    LauncherAppWidgetProviderInfo.fromProviderInfo(context, pInfo);
    

    方法进行初始化的,我们再看LauncherAppWidgetProviderInfo又继承AppWidgetProviderInfo,越来越怪,我们接着看fromProviderInfo(context, pInfo)方法:

    public static LauncherAppWidgetProviderInfo fromProviderInfo(Context context,
                AppWidgetProviderInfo info) {
            Parcel p = Parcel.obtain();
            info.writeToParcel(p, 0);
            p.setDataPosition(0);
            LauncherAppWidgetProviderInfo lawpi = new LauncherAppWidgetProviderInfo(p);
            p.recycle();
            return lawpi;
        }
    

    我们看到最后是通过new LauncherAppWidgetProviderInfo来生成一个LauncherAppWidgetProviderInfo对象,那么这个对象构造函数中有什么:

        public LauncherAppWidgetProviderInfo(Parcel in) {
            super(in);
            initSpans();
        }
    

    这个构造函数调用了initSpans方法,我们接着追寻:

    private void initSpans() {
            LauncherAppState app = LauncherAppState.getInstance();
            InvariantDeviceProfile idp = app.getInvariantDeviceProfile();
    
            // We only care out the cell size, which is independent of the the layout direction.
            Rect paddingLand = idp.landscapeProfile.getWorkspacePadding(false /* isLayoutRtl */);
            Rect paddingPort = idp.portraitProfile.getWorkspacePadding(false /* isLayoutRtl */);
    
            // Always assume we're working with the smallest span to make sure we
            // reserve enough space in both orientations.
            float smallestCellWidth = DeviceProfile.calculateCellWidth(Math.min(
                    idp.landscapeProfile.widthPx - paddingLand.left - paddingLand.right,
                    idp.portraitProfile.widthPx - paddingPort.left - paddingPort.right),
                    idp.numColumns);
            float smallestCellHeight = DeviceProfile.calculateCellWidth(Math.min(
                    idp.landscapeProfile.heightPx - paddingLand.top - paddingLand.bottom,
                    idp.portraitProfile.heightPx - paddingPort.top - paddingPort.bottom),
                    idp.numRows);
    
            // We want to account for the extra amount of padding that we are adding to the widget
            // to ensure that it gets the full amount of space that it has requested.
            Rect widgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(
                    app.getContext(), provider, null);
            spanX = Math.max(1, (int) Math.ceil(
                            (minWidth + widgetPadding.left + widgetPadding.right) / smallestCellWidth));
            spanY = Math.max(1, (int) Math.ceil(
                    (minHeight + widgetPadding.top + widgetPadding.bottom) / smallestCellHeight));
    
            minSpanX = Math.max(1, (int) Math.ceil(
                    (minResizeWidth + widgetPadding.left + widgetPadding.right) / smallestCellWidth));
            minSpanY = Math.max(1, (int) Math.ceil(
                    (minResizeHeight + widgetPadding.top + widgetPadding.bottom) / smallestCellHeight));
        }
    

    这段代码也不难,是为了算四个参数:spanX、spanY、minSpanX、minSpanY,看过我前面博客的都知道这个spanX和spanY参数是什么,其实这个LauncherAppWidgetProviderInfo对象比系统自带的AppWidgetProviderInfo带有的就是多了这几个参数,也就是方便我们添加到桌面是计算占用位置。

    最后得到HashMap<ComponentKey, LauncherAppWidgetProviderInfo>这个Widget集合,最后通过

    mBgWidgetsModel.setWidgetsAndShortcuts(widgetsAndShortcuts);
    

    将这个集合放到WidgetsModel中:

    public void setWidgetsAndShortcuts(ArrayList<Object> rawWidgetsShortcuts) {
            ...
            HashMap<String, PackageItemInfo> tmpPackageItemInfos = new HashMap<>();
    
            // clear the lists.
            ...
    
            InvariantDeviceProfile idp = LauncherAppState.getInstance().getInvariantDeviceProfile();
    
            // add and update.
            for (Object o: rawWidgetsShortcuts) {
                String packageName = "";
                UserHandleCompat userHandle = null;
                ComponentName componentName = null;
                if (o instanceof LauncherAppWidgetProviderInfo) {
                    LauncherAppWidgetProviderInfo widgetInfo = (LauncherAppWidgetProviderInfo) o;
    
                    // Ensure that all widgets we show can be added on a workspace of this size
                    int minSpanX = Math.min(widgetInfo.spanX, widgetInfo.minSpanX);
                    int minSpanY = Math.min(widgetInfo.spanY, widgetInfo.minSpanY);
                    if (minSpanX <= (int) idp.numColumns &&
                        minSpanY <= (int) idp.numRows) {
                        componentName = widgetInfo.provider;
                        packageName = widgetInfo.provider.getPackageName();
                        userHandle = mAppWidgetMgr.getUser(widgetInfo);
                    } else {
                        ...
                        continue;
                    }
                } else if (o instanceof ResolveInfo) {
                    ResolveInfo resolveInfo = (ResolveInfo) o;
                    componentName = new ComponentName(resolveInfo.activityInfo.packageName,
                            resolveInfo.activityInfo.name);
                    packageName = resolveInfo.activityInfo.packageName;
                    userHandle = UserHandleCompat.myUserHandle();
                }
    
                if (componentName == null || userHandle == null) {
                    ...
                    continue;
                }
                ...
    
                PackageItemInfo pInfo = tmpPackageItemInfos.get(packageName);
                ArrayList<Object> widgetsShortcutsList = mWidgetsList.get(pInfo);
                if (widgetsShortcutsList != null) {
                    widgetsShortcutsList.add(o);
                } else {
                    widgetsShortcutsList = new ArrayList<>();
                    widgetsShortcutsList.add(o);
                    pInfo = new PackageItemInfo(packageName);
                    mIconCache.getTitleAndIconForApp(packageName, userHandle,
                            true /* userLowResIcon */, pInfo);
                    pInfo.titleSectionName = mIndexer.computeSectionName(pInfo.title);
                    mWidgetsList.put(pInfo, widgetsShortcutsList);
                    tmpPackageItemInfos.put(packageName,  pInfo);
                    mPackageItemInfos.add(pInfo);
                }
            }
    
            // 排序.
            ...
            }
        }
    

    在这里将不同应用的Widget放到同一个列表中然后在放到mWidgetsList中,以供应加载Widget列表。接着执行绑定过程,绑定过程我们在第三章墨香带你学Launcher之(三)-绑定屏幕、图标、文件夹和Widget介绍过,但是里面还有些东西在这里需要介绍一下,我们看源码知道其实Widget是通过适配器放置到WidgetsRecyclerView里面的,WidgetsRecyclerView是一个RecyclerView,而每个Widget视图是一个WidgetCell,那么WidgetCell是什么,我们看WidgetsListAdapter适配器,这个我们就不详细介绍了,在里面的onBindViewHolder方法中对WidgetCell进行了初始化,其中在里面会调动下面方法:

    widget.applyFromAppWidgetProviderInfo(info, mWidgetPreviewLoader);
    

    我们看看这个方法:

     public void applyFromAppWidgetProviderInfo(LauncherAppWidgetProviderInfo info,
                WidgetPreviewLoader loader) {
    
            InvariantDeviceProfile profile =
                    LauncherAppState.getInstance().getInvariantDeviceProfile();
            mInfo = info;
            // TODO(hyunyoungs): setup a cache for these labels.
            mWidgetName.setText(AppWidgetManagerCompat.getInstance(getContext()).loadLabel(info));
            int hSpan = Math.min(info.spanX, profile.numColumns);
            int vSpan = Math.min(info.spanY, profile.numRows);
            mWidgetDims.setText(String.format(mDimensionsFormatString, hSpan, vSpan));
            mWidgetPreviewLoader = loader;
        }
    

    上面代码通过mWidgetName.setText显示名字,通过mWidgetDims.setText显示大小。最后给mWidgetPreviewLoader赋值,我们看到这个loader是从WidgetsListAdapter中传递进来的,在WidgetsListAdapter中,是通过LauncherAppState.getInstance().getWidgetCache()获取的,其实这个loader是在LauncherAppState初始化的时候就初始化了。

    在WidgetCell初始化后调用了widget.ensurePreview()方法:

     public void ensurePreview() {
            ...
            int[] size = getPreviewSize();
            mActiveRequest = mWidgetPreviewLoader.getPreview(mInfo, size[0], size[1], this);
        }
    

    在这里调用mWidgetPreviewLoader.getPreview方法:

        public PreviewLoadRequest getPreview(final Object o, int previewWidth,
                int previewHeight, WidgetCell caller) {
            String size = previewWidth + "x" + previewHeight;
            WidgetCacheKey key = getObjectKey(o, size);
    
            PreviewLoadTask task = new PreviewLoadTask(key, o, previewWidth, previewHeight, caller);
            task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
            return new PreviewLoadRequest(task);
        }
    

    在这里执行了一个异步任务PreviewLoadTask,我们看一下这个异步任务,首先看doInBackground方法:

    ...
    preview = generatePreview(launcher, mInfo, unusedBitmap, mPreviewWidth, mPreviewHeight);
    ...
    

    接着看generatePreview方法:

        Bitmap generatePreview(Launcher launcher, Object info, Bitmap recycle,
                int previewWidth, int previewHeight) {
            if (info instanceof LauncherAppWidgetProviderInfo) {
                return generateWidgetPreview(launcher, (LauncherAppWidgetProviderInfo) info,
                        previewWidth, recycle, null);
            } else {
                return generateShortcutPreview(launcher,
                        (ResolveInfo) info, previewWidth, previewHeight, recycle);
            }
        }
    

    我们看到是生成一个Bitmap,然后调用generateWidgetPreview生成Bitmap:

    public Bitmap generateWidgetPreview(Launcher launcher, LauncherAppWidgetProviderInfo info,
                int maxPreviewWidth, Bitmap preview, int[] preScaledWidthOut) {
            // Load the preview image if possible
            if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE;
    
            Drawable drawable = null;
            if (info.previewImage != 0) {
                // 获取相对应的drawable
                drawable = mManager.loadPreview(info);
                ...
            }
    
            // Draw the scaled preview into the final bitmap
            int x = (preview.getWidth() - previewWidth) / 2;
            if (widgetPreviewExists) {
                drawable.setBounds(x, 0, x + previewWidth, previewHeight);
                drawable.draw(c);
            } else {
                ...
                for (int i = 0; i < spanX; i++, tx += tileW) {
                    float ty = 0;
                    for (int j = 0; j < spanY; j++, ty += tileH) {
                        dst.offsetTo(tx, ty);
                        c.drawBitmap(tileBitmap, src, dst, p);
                    }
                }
                ...
                try {
                    Drawable icon = mutateOnMainThread(mManager.loadIcon(info, mIconCache));
                    if (icon != null) {
                        ...
                        icon.draw(c);
                    }
                } catch (Resources.NotFoundException e) { }
                c.setBitmap(null);
            }
            int imageHeight = Math.min(preview.getHeight(), previewHeight + mProfileBadgeMargin);
            return mManager.getBadgeBitmap(info, preview, imageHeight);
        }
    

    整个过程就是从系统加载出Widget对应的Drawable然后绘制到Bitmap上面返回,然后在onPostExecute方法中将该图片添加到WidgetCell上面,就显示到了WidgetCell列表中。整个加载就完成了。

    Widget的添加:

    我们之前讲过,Widget列表最后是绑定到WidgetsContainerView中的,而我们将Widget放置到桌面是通过长按拖拽到桌面来完成的,因此我们可以知道添加的触发事件是通过长按事件来触发的,因为我们找到WidgetsContainerView中的长按事件:

        @Override
        public boolean onLongClick(View v) {
            
            ...
    
            boolean status = beginDragging(v);
            if (status && v.getTag() instanceof PendingAddWidgetInfo) {
                WidgetHostViewLoader hostLoader = new WidgetHostViewLoader(mLauncher, v);
                boolean preloadStatus = hostLoader.preloadWidget();
                ...
                mLauncher.getDragController().addDragListener(hostLoader);
            }
            return status;
        }
    

    首先调用beginDragging方法:

        private boolean beginDragging(View v) {
            if (v instanceof WidgetCell) {
                if (!beginDraggingWidget((WidgetCell) v)) {
                    return false;
                }
            } else {
                Log.e(TAG, "Unexpected dragging view: " + v);
            }
    
            // We don't enter spring-loaded mode if the drag has been cancelled
            if (mLauncher.getDragController().isDragging()) {
                // Go into spring loaded mode (must happen before we startDrag())
                mLauncher.enterSpringLoadedDragMode();
            }
    
            return true;
        }
    

    如果是Widget的视图(WidgetCell)也就是长按的是Widget布局则调用beginDraggingWidget方法:

    private boolean beginDraggingWidget(WidgetCell v) {
            WidgetImageView image = (WidgetImageView) v.findViewById(R.id.widget_preview);
            ...
    
            if (createItemInfo instanceof PendingAddWidgetInfo) {
                ...
                Bitmap icon = image.getBitmap();
                float minScale = 1.25f;
                int maxWidth = Math.min((int) (icon.getWidth() * minScale), size[0]);
    
                ...
                preview = getWidgetPreviewLoader().generateWidgetPreview(mLauncher,
                        createWidgetInfo.info, maxWidth, null, previewSizeBeforeScale);
    
                ...
                scale = bounds.width() / (float) preview.getWidth();
            } else {
                // shortcut
                ...
            }
    
            // Don't clip alpha values for the drag outline if we're using the default widget preview
            boolean clipAlpha = !(createItemInfo instanceof PendingAddWidgetInfo &&
                    (((PendingAddWidgetInfo) createItemInfo).previewImage == 0));
    
            // Start the drag
            mLauncher.lockScreenOrientation();
            mLauncher.getWorkspace().onDragStartedWithItem(createItemInfo, preview, clipAlpha);
            mDragController.startDrag(image, preview, this, createItemInfo,
                    bounds, DragController.DRAG_ACTION_COPY, scale);
    
            preview.recycle();
            return true;
        }
    

    上面代码中的generateWidgetPreview方法我们在上面已经讲过了,就是生产WidgetCell图片的,然后锁定屏幕旋转,然后调用onDragStartedWithItem方法:

        public void onDragStartedWithItem(PendingAddItemInfo info, Bitmap b, boolean clipAlpha) {
            int[] size = estimateItemSize(info, false);
    
            // The outline is used to visualize where the item will land if dropped
            mDragOutline = createDragOutline(b, DRAG_BITMAP_PADDING, size[0], size[1], clipAlpha);
        }
    

    整个方法在拖拽中讲过,就是在workspace中生成一个拖拽view的轮廓边框,然后调用mDragController.startDrag方法,之后的过程在拖拽章节中有很详细的讲解,所以在此不再重复了,没看过拖拽的可以去看拖拽过程详解。下面只是个提示过程。

    在放置到桌面时会调用onDrop方法,然后调用onDropExternal方法,然后调用addPendingItem方法:

    public void addPendingItem(PendingAddItemInfo info, long container, long screenId,
                                   int[] cell, int spanX, int spanY) {
            switch (info.itemType) {
                case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
                case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
                    int span[] = new int[2];
                    span[0] = spanX;
                    span[1] = spanY;
                    addAppWidgetFromDrop((PendingAddWidgetInfo) info,
                            container, screenId, cell, span);
                    break;
                ...
                }
        }
    

    如果是Widget则调用addAppWidgetFromDrop方法,然后调用addAppWidgetImpl方法,然后调用completeAddAppWidget方法,最后调用mWorkspace.addInScreen方法就讲WidgetCell添加到了桌面上。

    Widget的大小调节:

    我们在桌面上添加完Widget后,如果长按你会发现在Widget四个边缘会出现拖动框,如果拖动可以调节小插件的大小,那么这个拖动框在哪里添加的呢,我们看一下,其实这个方法是DragLayer中的addResizeFrame方法,这个方法是在Workspace中的onDrop方法中调用的,也就是放到桌面上的时候就添加了。

    我们看一下这个方法:

    public void addResizeFrame(ItemInfo itemInfo, LauncherAppWidgetHostView widget,
                CellLayout cellLayout) {
            AppWidgetResizeFrame resizeFrame = new AppWidgetResizeFrame(getContext(),
                    widget, cellLayout, this);
    
            LayoutParams lp = new LayoutParams(-1, -1);
            lp.customPosition = true;
    
            addView(resizeFrame, lp);
            mResizeFrames.add(resizeFrame);
    
            resizeFrame.snapToWidget(false);
        }
    

    首先创建AppWidgetResizeFrame对象,传入参数LauncherAppWidgetHostView、CellLayout,还有draglayer:

        public AppWidgetResizeFrame(Context context,
                LauncherAppWidgetHostView widgetView, CellLayout cellLayout, DragLayer dragLayer) {
    
            //初始化数据
            ...
            
            // 初始化左侧拖动点
            mLeftHandle = new ImageView(context);
            mLeftHandle.setImageResource(R.drawable.ic_widget_resize_handle);
            lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
                    Gravity.LEFT | Gravity.CENTER_VERTICAL);
            lp.leftMargin = handleMargin;
            addView(mLeftHandle, lp);
    
            // 初始化右侧拖动点
            // 初始化顶部拖动点
            // 初始化底部拖动点
    
            ...
        }
    

    拖动调整大小是在DragLayer中的onTouchEvent方法中:

        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            
            ...
            
            if (mCurrentResizeFrame != null) {
                handled = true;
                switch (action) {
                    case MotionEvent.ACTION_MOVE:
                        mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown);
                        break;
                    case MotionEvent.ACTION_CANCEL:
                    case MotionEvent.ACTION_UP:
                        mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown);
                        mCurrentResizeFrame.onTouchUp();
                        mCurrentResizeFrame = null;
                }
            }
            if (handled) return true;
            return mDragController.onTouchEvent(ev);
        }
    

    由上面代码可以看出拖拽的的时候调用visualizeResizeForDelta方法,手指抬起的时候调用visualizeResizeForDelta方法和onTouchUp方法,我们先看visualizeResizeForDelta方法:

      private void visualizeResizeForDelta(int deltaX, int deltaY, boolean onDismiss) {
            updateDeltas(deltaX, deltaY);
            DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
    
            if (mLeftBorderActive) {
                lp.x = mBaselineX + mDeltaX;
                lp.width = mBaselineWidth - mDeltaX;
            } else if (mRightBorderActive) {
                lp.width = mBaselineWidth + mDeltaX;
            }
    
            if (mTopBorderActive) {
                lp.y = mBaselineY + mDeltaY;
                lp.height = mBaselineHeight - mDeltaY;
            } else if (mBottomBorderActive) {
                lp.height = mBaselineHeight + mDeltaY;
            }
    
            resizeWidgetIfNeeded(onDismiss);
            requestLayout();
        }
    

    首先调用updateDeltas方法:

        public void updateDeltas(int deltaX, int deltaY) {
            if (mLeftBorderActive) {
                mDeltaX = Math.max(-mBaselineX, deltaX); 
                mDeltaX = Math.min(mBaselineWidth - 2 * mTouchTargetWidth, mDeltaX);
            } else if (mRightBorderActive) {
                mDeltaX = Math.min(mDragLayer.getWidth() - (mBaselineX + mBaselineWidth), deltaX);
                mDeltaX = Math.max(-mBaselineWidth + 2 * mTouchTargetWidth, mDeltaX);
            }
    
            if (mTopBorderActive) {
                mDeltaY = Math.max(-mBaselineY, deltaY);
                mDeltaY = Math.min(mBaselineHeight - 2 * mTouchTargetWidth, mDeltaY);
            } else if (mBottomBorderActive) {
                mDeltaY = Math.min(mDragLayer.getHeight() - (mBaselineY + mBaselineHeight), deltaY);
                mDeltaY = Math.max(-mBaselineHeight + 2 * mTouchTargetWidth, mDeltaY);
            }
        }
    

    主要是根据上下左右点来计算mDeltaX和mDeltaY的值,然后设定DragLayer.LayoutParams的值,然后调用resizeWidgetIfNeeded方法:

    private void resizeWidgetIfNeeded(boolean onDismiss) {
            ...
            
            if (mLeftBorderActive) {
                cellXInc = Math.max(-cellX, hSpanInc);
                cellXInc = Math.min(lp.cellHSpan - mMinHSpan, cellXInc);
                hSpanInc *= -1;
                hSpanInc = Math.min(cellX, hSpanInc);
                hSpanInc = Math.max(-(lp.cellHSpan - mMinHSpan), hSpanInc);
                hSpanDelta = -hSpanInc;
    
            }
    
            ...
            
            // Update the widget's dimensions and position according to the deltas computed above
            if (mLeftBorderActive || mRightBorderActive) {
                spanX += hSpanInc;
                cellX += cellXInc;
                if (hSpanDelta != 0) {
                    mDirectionVector[0] = mLeftBorderActive ? -1 : 1;
                }
            }
    
            ...
    
            if (mCellLayout.createAreaForResize(cellX, cellY, spanX, spanY, mWidgetView,
                    mDirectionVector, onDismiss)) {
                lp.tmpCellX = cellX;
                lp.tmpCellY = cellY;
                lp.cellHSpan = spanX;
                lp.cellVSpan = spanY;
                mRunningVInc += vSpanDelta;
                mRunningHInc += hSpanDelta;
                if (!onDismiss) {
                    updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY);
                }
            }
            mWidgetView.requestLayout();
        }
    

    这里计算拖拽过程中的参数,然后调用updateWidgetSizeRanges方法:

        static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher,
                int spanX, int spanY) {
            getWidgetSizeRanges(launcher, spanX, spanY, sTmpRect);
            widgetView.updateAppWidgetSize(null, sTmpRect.left, sTmpRect.top,
                    sTmpRect.right, sTmpRect.bottom);
        }
    

    首先调用getWidgetSizeRanges方法来设定sTmpRect参数,然后调用widgetView.updateAppWidgetSize方法更新widget大小,然后调用mWidgetView.requestLayout方法刷新widget。

    我们再看onTouchUp方法:

        public void onTouchUp() {
            int xThreshold = mCellLayout.getCellWidth() + mCellLayout.getWidthGap();
            int yThreshold = mCellLayout.getCellHeight() + mCellLayout.getHeightGap();
    
            ...
    
            post(new Runnable() {
                @Override
                public void run() {
                    snapToWidget(true);
                }
            });
        }
    

    这个方法是调整完widget大小手指离开屏幕时调用的,主要调用了snapToWidget方法,这个方法代码就不贴了,主要是四个点的动画,代码很简单。

    到此widget的加载、添加以及大小调整就介绍完了,整个过程也是比较复杂的,所以还是要好好熟悉一下。

    最后

    同步发布:http://www.codemx.cn/2016/12/18/Launcher07/

    Github地址:https://github.com/yuchuangu85/Launcher3_mx

    微信公众账号:Code-MX

    qr_code_mx.jpg

    注:本文原创,转载请注明出处,多谢。

    相关文章

      网友评论

      • 姑娘请别为难小僧:写的不错!期待下期更新! 问下有无launcher改动较大的开源的学习下
        e7c435aaefb4:你好,原生的widget 显示在launcher 上会有padding,getDafult获取默认的参数,我发现取消了这个参数,界面上还是有padding效果,请问还需要修改哪些坐标参数么?
        翰墨飘香:@姑娘请别为难小僧 只有官方原版的

      本文标题:墨香带你学Launcher之(七)--小部件的加载、添加以及大小

      本文链接:https://www.haomeiwen.com/subject/dpuhvttx.html