墨香帶你學(xué)Launcher之(七)--小部件的加載雏搂、添加以及大小調(diào)節(jié)

上一章墨香帶你學(xué)Launcher之(六)--拖拽我們介紹了Launcher的拖拽過程,涉及到的范圍比較廣辛块,包括圖標(biāo)的拖拽畔派,桌面上CellLayout的拖拽,小部件的拖拽润绵,以及跨不同部件的拖拽线椰,設(shè)計(jì)思想非常巧妙,不過整個流程相對也比較好掌握尘盼,只要跟著上一章的流程自己多跟蹤幾遍基本就熟悉了憨愉。按照計(jì)劃本章我們繼續(xù)學(xué)習(xí)Launcher的Widget的加載烦绳、添加以及Widget的大小調(diào)節(jié)。

Widget的數(shù)據(jù)加載

其實(shí)我們在第二章墨香帶你學(xué)Launcher之(二)-數(shù)據(jù)加載流程介紹過Widget數(shù)據(jù)的加載配紫,相對只是簡單的做了介紹径密,下面我們稍微講的詳細(xì)點(diǎn)。

我們知道Widget的數(shù)據(jù)加載開始在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);
    }

上面代碼我們可以看到是通過調(diào)用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的初始化有兩個,一個是當(dāng)Api版本高于21(包含21)時于个,用AppWidgetManagerCompatVL氛魁,低于21時用AppWidgetManagerCompatV16,這兩個有什么不同厅篓,我們下面分析秀存。

下面我們看如何獲取Widget列表對象:

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

getAllProviders()方法是一個抽象方法,所以我們看哪里進(jìn)行了復(fù)寫羽氮,

launcher02.png

可以看到還是上面兩個兼容類復(fù)寫了該方法或链,我們看這個兩個類中做了什么處理,先看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語句株扛,我們可以看到當(dāng)Api大于等于17時,調(diào)用第一個進(jìn)行初始化汇荐,否則調(diào)用第二個方法進(jìn)行初始化洞就,這就是對不同手機(jī)版本做的兼容。在我們寫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循環(huán)來獲取的,其中有個UserHandle革娄,那么在源碼中給出的解釋是設(shè)備中的每個用戶倾贰,個人理解應(yīng)該是每個應(yīng)用,每個應(yīng)用會有0-N個Widget拦惋,也就是從每個應(yīng)用中獲取每個應(yīng)用的Widget列表匆浙。這樣for循環(huán)就可以獲取整個手機(jī)中所有應(yīng)用的widget列表了。

再回到上面getWidgetProviders方法的代碼中厕妖,我們接著看首尼,接著for循環(huán)AppWidgetProviderInfo列表信息,重構(gòu)LauncherAppWidgetProviderInfo對象,這里有點(diǎn)怪软能,為啥有了AppWidgetProviderInfo對象還要重構(gòu)一個LauncherAppWidgetProviderInfo對象迎捺,我們知道在寫插件的時候每個Widget都會有一個類繼承AppWidgetProvider,這樣才會有一個插件查排,因此我們知道AppWidgetProviderInfo對象肯定是AppWidgetProvider的對象凳枝,那么LauncherAppWidgetProviderInfo是什么,我們接著看能不能找到答案跋核,LauncherAppWidgetProviderInfo的初始化時通過

LauncherAppWidgetProviderInfo.fromProviderInfo(context, pInfo);

方法進(jìn)行初始化的,我們再看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對象砂代,那么這個對象構(gòu)造函數(shù)中有什么:

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

這個構(gòu)造函數(shù)調(diào)用了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));
    }

這段代碼也不難,是為了算四個參數(shù):spanX泊藕、spanY、minSpanX难礼、minSpanY娃圆,看過我前面博客的都知道這個spanX和spanY參數(shù)是什么,其實(shí)這個LauncherAppWidgetProviderInfo對象比系統(tǒng)自帶的AppWidgetProviderInfo帶有的就是多了這幾個參數(shù)蛾茉,也就是方便我們添加到桌面是計(jì)算占用位置讼呢。

最后得到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);
            }
        }

        // 排序.
        ...
        }
    }

在這里將不同應(yīng)用的Widget放到同一個列表中然后在放到mWidgetsList中谦炬,以供應(yīng)加載Widget列表悦屏。接著執(zhí)行綁定過程,綁定過程我們在第三章墨香帶你學(xué)Launcher之(三)-綁定屏幕键思、圖標(biāo)础爬、文件夾和Widget介紹過,但是里面還有些東西在這里需要介紹一下吼鳞,我們看源碼知道其實(shí)Widget是通過適配器放置到WidgetsRecyclerView里面的看蚜,WidgetsRecyclerView是一個RecyclerView,而每個Widget視圖是一個WidgetCell赔桌,那么WidgetCell是什么供炎,我們看WidgetsListAdapter適配器,這個我們就不詳細(xì)介紹了疾党,在里面的onBindViewHolder方法中對WidgetCell進(jìn)行了初始化音诫,其中在里面會調(diào)動下面方法:

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中傳遞進(jìn)來的,在WidgetsListAdapter中,是通過LauncherAppState.getInstance().getWidgetCache()獲取的蜓氨,其實(shí)這個loader是在LauncherAppState初始化的時候就初始化了聋袋。

在WidgetCell初始化后調(diào)用了widget.ensurePreview()方法:

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

在這里調(diào)用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);
    }

在這里執(zhí)行了一個異步任務(wù)PreviewLoadTask,我們看一下這個異步任務(wù)穴吹,首先看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幽勒,然后調(diào)用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) {
            // 獲取相對應(yīng)的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);
    }

整個過程就是從系統(tǒng)加載出Widget對應(yīng)的Drawable然后繪制到Bitmap上面返回,然后在onPostExecute方法中將該圖片添加到WidgetCell上面港令,就顯示到了WidgetCell列表中啥容。整個加載就完成了。

Widget的添加:

我們之前講過咪惠,Widget列表最后是綁定到WidgetsContainerView中的,而我們將Widget放置到桌面是通過長按拖拽到桌面來完成的淋淀,因此我們可以知道添加的觸發(fā)事件是通過長按事件來觸發(fā)的遥昧,因?yàn)槲覀冋业絎idgetsContainerView中的長按事件:

    @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;
    }

首先調(diào)用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布局則調(diào)用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方法我們在上面已經(jīng)講過了,就是生產(chǎn)WidgetCell圖片的朵纷,然后鎖定屏幕旋轉(zhuǎn)炭臭,然后調(diào)用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的輪廓邊框袍辞,然后調(diào)用mDragController.startDrag方法鞋仍,之后的過程在拖拽章節(jié)中有很詳細(xì)的講解,所以在此不再重復(fù)了搅吁,沒看過拖拽的可以去看拖拽過程詳解威创。下面只是個提示過程。

在放置到桌面時會調(diào)用onDrop方法谎懦,然后調(diào)用onDropExternal方法肚豺,然后調(diào)用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則調(diào)用addAppWidgetFromDrop方法,然后調(diào)用addAppWidgetImpl方法党瓮,然后調(diào)用completeAddAppWidget方法详炬,最后調(diào)用mWorkspace.addInScreen方法就講WidgetCell添加到了桌面上。

Widget的大小調(diào)節(jié):

我們在桌面上添加完Widget后寞奸,如果長按你會發(fā)現(xiàn)在Widget四個邊緣會出現(xiàn)拖動框呛谜,如果拖動可以調(diào)節(jié)小插件的大小,那么這個拖動框在哪里添加的呢枪萄,我們看一下隐岛,其實(shí)這個方法是DragLayer中的addResizeFrame方法,這個方法是在Workspace中的onDrop方法中調(diào)用的瓷翻,也就是放到桌面上的時候就添加了聚凹。

我們看一下這個方法:

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);
    }

首先創(chuàng)建AppWidgetResizeFrame對象割坠,傳入?yún)?shù)LauncherAppWidgetHostView、CellLayout妒牙,還有draglayer:

    public AppWidgetResizeFrame(Context context,
            LauncherAppWidgetHostView widgetView, CellLayout cellLayout, DragLayer dragLayer) {

        //初始化數(shù)據(jù)
        ...
        
        // 初始化左側(cè)拖動點(diǎn)
        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);

        // 初始化右側(cè)拖動點(diǎn)
        // 初始化頂部拖動點(diǎn)
        // 初始化底部拖動點(diǎn)

        ...
    }

拖動調(diào)整大小是在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);
    }

由上面代碼可以看出拖拽的的時候調(diào)用visualizeResizeForDelta方法彼哼,手指抬起的時候調(diào)用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();
    }

首先調(diào)用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);
        }
    }

主要是根據(jù)上下左右點(diǎn)來計(jì)算mDeltaX和mDeltaY的值湘今,然后設(shè)定DragLayer.LayoutParams的值敢朱,然后調(diào)用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();
    }

這里計(jì)算拖拽過程中的參數(shù),然后調(diào)用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);
    }

首先調(diào)用getWidgetSizeRanges方法來設(shè)定sTmpRect參數(shù)摩瞎,然后調(diào)用widgetView.updateAppWidgetSize方法更新widget大小拴签,然后調(diào)用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);
            }
        });
    }

這個方法是調(diào)整完widget大小手指離開屏幕時調(diào)用的旗们,主要調(diào)用了snapToWidget方法蚓哩,這個方法代碼就不貼了,主要是四個點(diǎn)的動畫上渴,代碼很簡單岸梨。

到此widget的加載、添加以及大小調(diào)整就介紹完了稠氮,整個過程也是比較復(fù)雜的盛嘿,所以還是要好好熟悉一下。

最后

同步發(fā)布:http://www.codemx.cn/2016/12/18/Launcher07/

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

微信公眾賬號:Code-MX

qr_code_mx.jpg

注:本文原創(chuàng)括袒,轉(zhuǎn)載請注明出處,多謝稿茉。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锹锰,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子漓库,更是在濱河造成了極大的恐慌恃慧,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件渺蒿,死亡現(xiàn)場離奇詭異痢士,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)茂装,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評論 3 385
  • 文/潘曉璐 我一進(jìn)店門怠蹂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人少态,你說我怎么就攤上這事城侧。” “怎么了彼妻?”我有些...
    開封第一講書人閱讀 157,435評論 0 348
  • 文/不壞的土叔 我叫張陵嫌佑,是天一觀的道長豆茫。 經(jīng)常有香客問我,道長屋摇,這世上最難降的妖魔是什么揩魂? 我笑而不...
    開封第一講書人閱讀 56,509評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮炮温,結(jié)果婚禮上火脉,老公的妹妹穿的比我還像新娘。我一直安慰自己茅特,他們只是感情好忘分,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,611評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著白修,像睡著了一般妒峦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上兵睛,一...
    開封第一講書人閱讀 49,837評論 1 290
  • 那天肯骇,我揣著相機(jī)與錄音,去河邊找鬼祖很。 笑死笛丙,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的假颇。 我是一名探鬼主播胚鸯,決...
    沈念sama閱讀 38,987評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼笨鸡!你這毒婦竟也來了姜钳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,730評論 0 267
  • 序言:老撾萬榮一對情侶失蹤形耗,失蹤者是張志新(化名)和其女友劉穎哥桥,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體激涤,經(jīng)...
    沈念sama閱讀 44,194評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拟糕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,525評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了倦踢。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片送滞。...
    茶點(diǎn)故事閱讀 38,664評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖辱挥,靈堂內(nèi)的尸體忽然破棺而出累澡,到底是詐尸還是另有隱情,我是刑警寧澤般贼,帶...
    沈念sama閱讀 34,334評論 4 330
  • 正文 年R本政府宣布愧哟,位于F島的核電站奥吩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蕊梧。R本人自食惡果不足惜霞赫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,944評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望肥矢。 院中可真熱鬧端衰,春花似錦、人聲如沸甘改。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,764評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽十艾。三九已至抵代,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間忘嫉,已是汗流浹背荤牍。 一陣腳步聲響...
    開封第一講書人閱讀 31,997評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留庆冕,地道東北人康吵。 一個月前我還...
    沈念sama閱讀 46,389評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像访递,于是被迫代替她去往敵國和親晦嵌。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,554評論 2 349

推薦閱讀更多精彩內(nèi)容