android13#launcher3#folder

169 阅读11分钟

1.简介

学习下folder的图标咋弄的

1.1.CellLayout.java

自定义的容器

public class CellLayout extends ViewGroup {

>1.构造方法

构造方法里默认添加了一个容器

    public CellLayout(Context context, AttributeSet attrs, int defStyle) {
    //...
    //自定义的容器
        mShortcutsAndWidgets = new ShortcutAndWidgetContainer(context, mContainerType);
        mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
                mBorderSpace);
       //加入CellLayout
        addView(mShortcutsAndWidgets);
    }

>2.addViewToCellLayout

添加child的方法,可以看到,最终是加入到mShortcutsAndWidgets里了

    public boolean addViewToCellLayout(View child, int index, int childId,
            CellLayoutLayoutParams params, boolean markCells) {
        final CellLayoutLayoutParams lp = params;

        // Hotseat不显示文字
        if (child instanceof BubbleTextView) {
            BubbleTextView bubbleChild = (BubbleTextView) child;
            bubbleChild.setTextVisibility(mContainerType != HOTSEAT);
        }

        child.setScaleX(DEFAULT_SCALE);
        child.setScaleY(DEFAULT_SCALE);

        if (lp.getCellX() >= 0 && lp.getCellX() <= mCountX - 1
                && lp.getCellY() >= 0 && lp.getCellY() <= mCountY - 1) {
            // If the horizontal or vertical span is set to -1, it is taken to
            // mean that it spans the extent of the CellLayout
            if (lp.cellHSpan < 0) lp.cellHSpan = mCountX;
            if (lp.cellVSpan < 0) lp.cellVSpan = mCountY;

            child.setId(childId);
    //加入内部容器mShortcutsAndWidgets里
            mShortcutsAndWidgets.addView(child, index, lp);

            if (markCells) markCellsAsOccupiedForView(child);

            return true;
        }
        return false;
    }

文件夹可能存在的地方有3处,workspace,hotseat,taskbar

1.2..Workspace.java

  • 自定义的ViewGroup,分页的,每页添加的其实就是1.1里的CellLayout
  • 数据添加参考1.3.1
public class Workspace<T extends View & PageIndicator> extends PagedView<T>

>1.insertNewWorkspaceScreen

可以看到Workspace添加的child就是CellLayout

    public CellLayout insertNewWorkspaceScreen(int screenId, int insertIndex) {

        DeviceProfile dp = mLauncher.getDeviceProfile();
        CellLayout newScreen;
        if (FOLDABLE_SINGLE_PAGE.get() && dp.isTwoPanels) {
            newScreen = (CellLayout) LayoutInflater.from(getContext()).inflate(
                    R.layout.workspace_screen_foldable, this, false /* attachToRoot */);
        } else {
        //看这里,添加的就是CellLayout
            newScreen = (CellLayout) LayoutInflater.from(getContext()).inflate(
                    R.layout.workspace_screen, this, false /* attachToRoot */);
        }
        newScreen.setCellLayoutContainer(this);

        mWorkspaceScreens.put(screenId, newScreen);
        mScreenOrder.add(insertIndex, screenId);
        //加入容器里
        addView(newScreen, insertIndex);
        mStateTransitionAnimation.applyChildState(
                mLauncher.getStateManager().getState(), newScreen, insertIndex);

        updatePageScrollValues();
        updateCellLayoutMeasures();
        return newScreen;
    }

1.3.Hotseat

public class Hotseat extends CellLayout implements Insettable {

>1.数据添加

参考WorkspaceLayoutManager.java,Launcher里bind数据的时候会调用

    default void addInScreen(View child, int container, int screenId, int x, int y,
            int spanX, int spanY) {
            //..
        final CellLayout layout;
        //判断容器类型
        if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
                || container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
                //获取hotseat容器
            layout = getHotseat();

            // Hide folder title in the hotseat
            if (child instanceof FolderIcon) {
                ((FolderIcon) child).setTextVisible(false);
            }
        } else {
            // Show folder title if not in the hotseat
            if (child instanceof FolderIcon) {
                ((FolderIcon) child).setTextVisible(true);
            }
            //这里是根据workspace的页码获取容器,就是1.2.1添加的
            layout = getScreenWithId(screenId);
        }            
//..//可以看到,调用的是1.1.2的方法添加child
        if (!layout.addViewToCellLayout(child, -1, childId, lp, markCellsAsOccupied)) {
        }
        //..

1.4.TaskbarView

public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconParent, Insettable,

>1.updateHotseatItems

    protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
    //..
        for (int i = 0; i < hotseatItemInfos.length; i++) {
            ItemInfo hotseatItemInfo = hotseatItemInfos[i];
            if (hotseatItemInfo == null) {
                continue;
            }

            //根据数据类型加载不同的布局
            final int expectedLayoutResId;
            boolean isCollection = false;
            //推荐应用
            if (hotseatItemInfo.isPredictedItem()) {
                expectedLayoutResId = R.layout.taskbar_predicted_app_icon;
            } else if (hotseatItemInfo instanceof FolderInfo fi) {
            //文件夹
                expectedLayoutResId = fi.itemType == ITEM_TYPE_APP_PAIR
                        ? R.layout.app_pair_icon
                        : R.layout.folder_icon;
                isCollection = true;
            } else {
            //正常的应用
                expectedLayoutResId = R.layout.taskbar_app_icon;
            }    
    

2.FolderIcon.java

public class FolderIcon extends FrameLayout implements FolderListener, IconLabelDotView,
        DraggableView, Reorderable {

    public int getViewType() {
        return DRAGGABLE_ICON;
    }

2.1.哪里用到了

>1.TaskbarView.java

                if (isFolder) {
                    FolderInfo folderInfo = (FolderInfo) hotseatItemInfo;
                    //其他几处也用的这个静态方法获取的,见2.3
                    FolderIcon folderIcon = FolderIcon.inflateFolderAndIcon(expectedLayoutResId,
                            mActivityContext, this, folderInfo);
                    //见2.7,隐藏文本控件
                    folderIcon.setTextVisible(false);
                    hotseatView = folderIcon;
                }

2.2.构造方法

    public FolderIcon(Context context) {
        super(context);
        init();
    }

    private void init() {
        mLongPressHelper = new CheckLongPressHelper(this);
        mPreviewLayoutRule = new ClippedFolderIconLayoutRule();
        //预览管理器
        mPreviewItemManager = new PreviewItemManager(this);
        //未读消息那个点
        mDotParams = new DotRenderer.DrawParams();
    }

2.3.inflateFolderAndIcon

    public static <T extends Context & ActivityContext> FolderIcon inflateFolderAndIcon(int resId,
            T activityContext, ViewGroup group, FolderInfo folderInfo) {
        //见4.1,加载Folder布局
        Folder folder = Folder.fromXml(activityContext);
        //见补充2,加载FolderIcon布局
        FolderIcon icon = inflateIcon(resId, activityContext, group, folderInfo);
        //见4.3
        folder.setFolderIcon(icon);
        //绑定数据
        folder.bind(folderInfo);
        
        icon.setFolder(folder);
        return icon;
    }

>1.folder_icon.xml

  • 目前用的布局是这个,自定义的帧布局,里边套一个自定义的文本控件
  • folder_icon_name用来显示文件夹的名字的,默认是空的
<com.android.launcher3.folder.FolderIcon
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:focusable="true" >
    <com.android.launcher3.views.DoubleShadowBubbleTextView
        style="@style/BaseIcon.Workspace"
        android:id="@+id/folder_icon_name"
        android:focusable="false"
        android:layout_gravity="top"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</com.android.launcher3.folder.FolderIcon>

>2.inflateIcon

    public static FolderIcon inflateIcon(int resId, ActivityContext activity, ViewGroup group,
            FolderInfo folderInfo) {

        DeviceProfile grid = activity.getDeviceProfile();
        //加载布局
        FolderIcon icon = (FolderIcon) LayoutInflater.from(group.getContext())
                .inflate(resId, group, false);

        icon.setClipToPadding(false);
        //见补充1里的布局,里边的文本控件
        icon.mFolderName = icon.findViewById(R.id.folder_icon_name);
        icon.mFolderName.setText(folderInfo.title);
        icon.mFolderName.setCompoundDrawablePadding(0);
        //文本控件的布局参数
        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams();
        //顶部margin等于图标的大小加上间距,所以正常情况显示在文件夹的底部
        lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx;

        icon.setTag(folderInfo);
        //点击事件
        icon.setOnClickListener(ItemClickHandler.INSTANCE);
        icon.mInfo = folderInfo;
        icon.mActivity = activity;
        icon.mDotRenderer = grid.mDotRendererWorkSpace;

        icon.setContentDescription(icon.getAccessiblityTitle(folderInfo.title));

        //
        FolderDotInfo folderDotInfo = new FolderDotInfo();
        for (WorkspaceItemInfo si : folderInfo.contents) {
            folderDotInfo.addDotInfo(activity.getDotInfoForItem(si));
        }
        icon.setDotInfo(folderDotInfo);

        icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
        //见2.4,网格组织者,
        icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile().inv);
        //设置folder信息
        icon.mPreviewVerifier.setFolderInfo(folderInfo);
        //补充3
        icon.updatePreviewItems(false);

        folderInfo.addListener(icon);

        return icon;
    }

>3.updatePreviewItems

    private void updatePreviewItems(boolean animate) {
        mPreviewItemManager.updatePreviewItems(animate);
        mCurrentPreviewItems.clear();
        mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0));
    }

2.4.FolderGridOrganizer.java

>1.构造方法

  • 读取的是配置文件里的数据device_profiles.xml
    public static final int MAX_NUM_ITEMS_IN_PREVIEW = 4;
    private static final int MIN_NUM_ITEMS_IN_PREVIEW = 2;
    
    public FolderGridOrganizer(InvariantDeviceProfile profile) {
        //读取folder里要显示的行数和列数
        mMaxCountX = profile.numFolderColumns;
        mMaxCountY = profile.numFolderRows;
        //每页最多显示几个
        mMaxItemsPerPage = mMaxCountX * mMaxCountY;
    }

>2.setFolderInfo

    public FolderGridOrganizer setFolderInfo(FolderInfo info) {
        //见补充3,设置folder里有几个图标
        return setContentSize(info.contents.size());
    }

>3.setContentSize

    public FolderGridOrganizer setContentSize(int contentSize) {
        if (contentSize != mNumItemsInFolder) {
        //计算网格大小,见补充4
            calculateGridSize(contentSize);
            //是否超过最大预览数,这里是4个
            mDisplayingUpperLeftQuadrant = contentSize > MAX_NUM_ITEMS_IN_PREVIEW;
            mNumItemsInFolder = contentSize;
        }
        return this;
    }

>4.calculateGridSize

根据数据个数,计算显示的行和列

    private void calculateGridSize(int count) {
        boolean done;
        int gridCountX = mCountX;
        int gridCountY = mCountY;
        //文件夹里包含的图标数超过了每页的最大值
        if (count >= mMaxItemsPerPage) {
            //设置为最大行 列
            gridCountX = mMaxCountX;
            gridCountY = mMaxCountY;
            done = true;
        } else {
            done = false;
        }

        while (!done) {
            int oldCountX = gridCountX;
            int oldCountY = gridCountY;
            if (gridCountX * gridCountY < count) {
                // Current grid is too small, expand it
                if ((gridCountX <= gridCountY || gridCountY == mMaxCountY)
                        && gridCountX < mMaxCountX) {
                    //先增加列
                    gridCountX++;
                } else if (gridCountY < mMaxCountY) {
                //再增加行
                    gridCountY++;
                }
                //至少有一行
                if (gridCountY == 0) gridCountY++;
            } else if ((gridCountY - 1) * gridCountX >= count && gridCountY >= gridCountX) {
                gridCountY = Math.max(0, gridCountY - 1);
            } else if ((gridCountX - 1) * gridCountY >= count) {
                gridCountX = Math.max(0, gridCountX - 1);
            }
            done = gridCountX == oldCountX && gridCountY == oldCountY;
        }
        //最终的行和列
        mCountX = gridCountX;
        mCountY = gridCountY;
    }

>5.previewItemsForPage

根据提供的页码以及数据集合,找到对应页面的数据

    public <T, R extends T> ArrayList<R> previewItemsForPage(int page, List<T> contents) {
        ArrayList<R> result = new ArrayList<>();
        //这个就是每页可以最多显示的个数
        int itemsPerPage = mCountX * mCountY;
        //乘以页面
        int start = itemsPerPage * page;
        //计算数据end
        int end = Math.min(start + itemsPerPage, contents.size());

        for (int i = start, rank = 0; i < end; i++, rank++) {
            if (isItemInPreview(page, rank)) {
                result.add((R) contents.get(i));
            }

            if (result.size() == MAX_NUM_ITEMS_IN_PREVIEW) {
                break;
            }
        }
        return result;
    }

>6.isItemInPreview

根据页码以及排名返回图标是否在预览图里

    public boolean isItemInPreview(int page, int rank) {
        //页码大于0或者数据量大于4
        if (page > 0 || mDisplayingUpperLeftQuadrant) {
            int col = rank % mCountX;
            int row = rank / mCountX;
            return col < 2 && row < 2;
        }
        return rank < MAX_NUM_ITEMS_IN_PREVIEW;
    }

2.5.dispatchDraw

    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        //背景不可见,点击展开的时候
        if (!mBackgroundIsVisible) return;

        mPreviewItemManager.recomputePreviewDrawingParams();
        //见5.2
        if (!mBackground.drawingDelegated()) {
            //这个就是画FolderIcon的半透明的背景,见5.1
            mBackground.drawBackground(canvas);
        }

        if (mCurrentPreviewItems.isEmpty() && !mAnimating) return;
        //这个是画小图标的,见3.4
        mPreviewItemManager.draw(canvas);

        if (!mBackground.drawingDelegated()) {
            //背景边框,见5.3,默认不画
            mBackground.drawBackgroundStroke(canvas);
        }
        //画圆点的
        drawDot(canvas);
    }

2.6.drawDot

    PreviewBackground mBackground = new PreviewBackground();
    
    public void drawDot(Canvas canvas) {
        if (!mForceHideDot && ((mDotInfo != null && mDotInfo.hasDot()) || mDotScale > 0)) {
            Rect iconBounds = mDotParams.iconBounds;
            // FolderIcon draws the icon to be top-aligned (with padding) & horizontally-centered
            int iconSize = mActivity.getDeviceProfile().iconSizePx;
            iconBounds.left = (getWidth() - iconSize) / 2;
            iconBounds.right = iconBounds.left + iconSize;
            iconBounds.top = getPaddingTop();
            iconBounds.bottom = iconBounds.top + iconSize;

            float iconScale = (float) mBackground.previewSize / iconSize;
            Utilities.scaleRectAboutCenter(iconBounds, iconScale);

            // If we are animating to the accepting state, animate the dot out.
            mDotParams.scale = Math.max(0, mDotScale - mBackground.getScaleProgress());
            mDotParams.dotColor = mBackground.getDotColor();
            mDotRenderer.draw(canvas, mDotParams);
        }
    }

2.7.setTextVisible

设置文本控件的可见性

    public void setTextVisible(boolean visible) {
        if (visible) {
            mFolderName.setVisibility(VISIBLE);
        } else {
            mFolderName.setVisibility(INVISIBLE);
        }
    }

3.PreviewItemManager.java

3.1.构造方法

    public PreviewItemManager(FolderIcon icon) {
        mContext = icon.getContext();
        mIcon = icon;
        //icon的大小
        mIconSize = ActivityContext.lookupContext(
                mContext).getDeviceProfile().folderChildIconSizePx;
        mClipThreshold = Utilities.dpToPx(1f);
    }

3.2.updatePreviewItems

    void updatePreviewItems(boolean animate) {
    //获取旧的预览个数
        int numOfPrevItemsAux = mFirstPageParams.size();
        buildParamsForPage(0, mFirstPageParams, animate);//补充1
        //记录旧的预览个数
        mNumOfPrevItems = numOfPrevItemsAux;
    }

>1.buildParamsForPage

构建对应页面的预览数据

    void buildParamsForPage(int page, ArrayList<PreviewItemDrawingParams> params, boolean animate) {
    //先获取对应页面的数据
        List<WorkspaceItemInfo> items = mIcon.getPreviewItemsOnPage(page);

        // We adjust the size of the list to match the number of items in the preview.
        //我们调整列表的大小以匹配预览中的项目数量
        while (items.size() < params.size()) {
            params.remove(params.size() - 1);//多了,删除
        }
        while (items.size() > params.size()) {
            params.add(new PreviewItemDrawingParams(0, 0, 0));//少了,添加
        }

        int numItemsInFirstPagePreview = page == 0 ? items.size() : MAX_NUM_ITEMS_IN_PREVIEW;
        for (int i = 0; i < params.size(); i++) {
            PreviewItemDrawingParams p = params.get(i);
            setDrawable(p, items.get(i));

            if (!animate) {
                if (p.anim != null) {
                    p.anim.cancel();
                }
                computePreviewItemDrawingParams(i, numItemsInFirstPagePreview, p);
                if (mReferenceDrawable == null) {
                    mReferenceDrawable = p.drawable;
                }
            } else {
                FolderPreviewItemAnim anim = new FolderPreviewItemAnim(this, p, i,
                        mNumOfPrevItems, i, numItemsInFirstPagePreview, DROP_IN_ANIMATION_DURATION,
                        null);

                if (p.anim != null) {
                    if (p.anim.hasEqualFinalState(anim)) {
                        // do nothing, let the current animation finish
                        continue;
                    }
                    p.anim.cancel();
                }
                p.anim = anim;
                p.anim.start();
            }
        }
    }

3.3.recomputePreviewDrawingParams

    public void recomputePreviewDrawingParams() {
        if (mReferenceDrawable != null) {
            computePreviewDrawingParams(mReferenceDrawable.getIntrinsicWidth(),
                    mIcon.getMeasuredWidth());
        }
    }

>1.computePreviewDrawingParams

    private void computePreviewDrawingParams(int drawableSize, int totalSize) {
        //下边3种数据发生变化
        if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize ||
                mPrevTopPadding != mIcon.getPaddingTop()) {
            mIntrinsicIconSize = drawableSize;
            mTotalWidth = totalSize;
            mPrevTopPadding = mIcon.getPaddingTop();
        
            mIcon.mBackground.setup(mIcon.getContext(), mIcon.mActivity, mIcon, mTotalWidth,
                    mIcon.getPaddingTop());
            mIcon.mPreviewLayoutRule.init(mIcon.mBackground.previewSize, mIntrinsicIconSize,
                    Utilities.isRtl(mIcon.getResources()));

            updatePreviewItems(false);
        }
    }

3.4.draw

    public void draw(Canvas canvas) {
        int saveCount = canvas.getSaveCount();
        // The items are drawn in coordinates relative to the preview offset
        PreviewBackground bg = mIcon.getFolderBackground();
        Path clipPath = bg.getClipPath();
        float firstPageItemsTransX = 0;
        //是否应该滑动到第一页:这个只有打开文件夹,滑动到非第一页,然后关闭文件夹的时候为true
        if (mShouldSlideInFirstPage) {
            PointF firstPageOffset = new PointF(bg.basePreviewOffsetX + mCurrentPageItemsTransX,
                    bg.basePreviewOffsetY);
            boolean shouldClip = mCurrentPageItemsTransX > mClipThreshold;
            drawParams(canvas, mCurrentPageParams, firstPageOffset, shouldClip, clipPath);
            firstPageItemsTransX = -ITEM_SLIDE_IN_OUT_DISTANCE_PX + mCurrentPageItemsTransX;
        }

        PointF firstPageOffset = new PointF(bg.basePreviewOffsetX + firstPageItemsTransX,
                bg.basePreviewOffsetY);
        boolean shouldClipFirstPage = firstPageItemsTransX < -mClipThreshold;
        //见补充1
        drawParams(canvas, mFirstPageParams, firstPageOffset, shouldClipFirstPage, clipPath);
        canvas.restoreToCount(saveCount);
    }

>1.drawParams

    public void drawParams(Canvas canvas, ArrayList<PreviewItemDrawingParams> params,
            PointF offset, boolean shouldClipPath, Path clipPath) {
        // The first item should be drawn last (ie. on top of later items)
        for (int i = params.size() - 1; i >= 0; i--) {
            PreviewItemDrawingParams p = params.get(i);
            if (!p.hidden) {
                // Exiting param should always be clipped.
                boolean isExiting = p.index == EXIT_INDEX;
                //见补充2
                drawPreviewItem(canvas, p, offset, isExiting | shouldClipPath, clipPath);
            }
        }
    }

>2.drawPreviewItem

    private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params, PointF offset,
            boolean shouldClipPath, Path clipPath) {
        canvas.save();
        if (shouldClipPath) {
            canvas.clipPath(clipPath);
        }
        canvas.translate(offset.x + params.transX, offset.y + params.transY);
        canvas.scale(params.scale, params.scale);
        Drawable d = params.drawable;

        if (d != null) {
            Rect bounds = d.getBounds();
            canvas.save();
            canvas.translate(-bounds.left, -bounds.top);
            canvas.scale(mIntrinsicIconSize / bounds.width(), mIntrinsicIconSize / bounds.height());
            d.draw(canvas);
            canvas.restore();
        }
        canvas.restore();
    }

3.5.效果图

对比两张图,可以看到,文件夹缩略图画的是左上角4个

>1.文件夹

image.png

>2.展开文件夹

image.png

4.Folder

  • 继承的是自定义的容器,线性布局,这个就是点击folderIcon图标展开以后看到的东西
public class Folder extends AbstractFloatingView implements ClipPathView, DragSource,

4.1.fromXml

    static <T extends Context & ActivityContext> Folder fromXml(T activityContext) {
        return (Folder) LayoutInflater.from(activityContext).cloneInContext(activityContext)
                .inflate(R.layout.user_folder_icon_normalized, null);
    }

>1.user_folder_icon_normalized

<com.android.launcher3.folder.Folder 
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical" >
<!--图标列表,可以翻页的-->
    <com.android.launcher3.folder.FolderPagedView
        android:id="@+id/folder_content"
        android:clipToPadding="false"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        launcher:pageIndicator="@+id/folder_page_indicator" />
<!--一个可编译的文本框,显示文件夹的名字,以及一个游标显示上边pagedView的页面-->
    <LinearLayout
        android:id="@+id/folder_footer"
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:clipChildren="false"
        android:orientation="horizontal"
        android:paddingLeft="12dp"
        android:paddingRight="12dp" >

        <com.android.launcher3.folder.FolderNameEditText
            android:id="@+id/folder_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            style="@style/TextHeadline"
            android:layout_weight="1"
            android:background="@android:color/transparent"
            android:gravity="center_horizontal"
            android:hint="@string/folder_hint_text"
            android:imeOptions="flagNoExtractUi"
            android:singleLine="true"
            android:textColor="?attr/folderTextColor"
            android:textColorHighlight="?android:attr/colorControlHighlight"
            android:textColorHint="?attr/folderHintColor"/>

        <com.android.launcher3.pageindicators.PageIndicatorDots
            android:id="@+id/folder_page_indicator"
            android:layout_gravity="center_vertical"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:elevation="1dp"
            />

    </LinearLayout>

</com.android.launcher3.folder.Folder>

4.2.构造方法

    public Folder(Context context, AttributeSet attrs) {
        super(context, attrs);
        setAlwaysDrawnWithCacheEnabled(false);

        mActivityContext = ActivityContext.lookupContext(context);
        mLauncherDelegate = LauncherDelegate.from(mActivityContext);

        mStatsLogManager = StatsLogManager.newInstance(context);
        setFocusableInTouchMode(true);
    }

4.3.setFolderIcon

    public void setFolderIcon(FolderIcon icon) {
        mFolderIcon = icon;
        //见4.4.1
        mLauncherDelegate.init(this, icon);
    }

4.4.LauncherDelegate.java

>1.init

    void init(Folder folder, FolderIcon icon) {
        folder.setDragController(mLauncher.getDragController());
        icon.setOnFocusChangeListener(mLauncher.getFocusHandler());
    }

4.5.bind

绑定数据

    void bind(FolderInfo info) {
        mInfo = info;
        mFromTitle = info.title;
        mFromLabelState = info.getFromLabelState();
        ArrayList<WorkspaceItemInfo> children = info.contents;
        Collections.sort(children, ITEM_POS_COMPARATOR);
        updateItemLocationsInDatabaseBatch(true);//补充1

        BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) getLayoutParams();
        if (lp == null) {
            lp = new BaseDragLayer.LayoutParams(0, 0);
            lp.customPosition = true;
            setLayoutParams(lp);
        }
        mItemsInvalidated = true;
        mInfo.addListener(this);

        if (!isEmpty(mInfo.title)) {
            mFolderName.setText(mInfo.title);
            mFolderName.setHint(null);
        } else {
            mFolderName.setText("");
            mFolderName.setHint(R.string.folder_hint_text);
        }
        // In case any children didn't come across during loading, clean up the folder accordingly
        mFolderIcon.post(() -> {
            if (getItemCount() <= 1) {
                replaceFolderWithFinalItem();
            }
        });
    }

>1.updateItemLocationsInDatabaseBatch

    private void updateItemLocationsInDatabaseBatch(boolean isBind) {
        FolderGridOrganizer verifier = new FolderGridOrganizer(
                mActivityContext.getDeviceProfile().inv).setFolderInfo(mInfo);

        ArrayList<ItemInfo> items = new ArrayList<>();
        int total = mInfo.contents.size();
        for (int i = 0; i < total; i++) {
            WorkspaceItemInfo itemInfo = mInfo.contents.get(i);
            if (verifier.updateRankAndPos(itemInfo, i)) {
                items.add(itemInfo);
            }
        }

        if (!items.isEmpty()) {
            mLauncherDelegate.getModelWriter().moveItemsInDatabase(items, mInfo.id, 0);
        }
        if (!isBind && total > 1 /* no need to update if there's one icon */) {
            Executors.MODEL_EXECUTOR.post(() -> {
                FolderNameInfos nameInfos = new FolderNameInfos();
                FolderNameProvider fnp = FolderNameProvider.newInstance(getContext());
                fnp.getSuggestedFolderName(
                        getContext(), mInfo.contents, nameInfos);
                mInfo.suggestedFolderNames = nameInfos;
            });
        }
    }

5.PreviewBackground.java

此对象表示FolderIcon预览背景。它存储绘图/测量*信息,处理绘图和动画(接受状态<- >休息状态)

5.1.drawBackground

    public void drawBackground(Canvas canvas) {
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(getBgColor());
        //shape见小节6,最终用的6.4的RoundedSquare
        getShape().drawShape(canvas, getOffsetX(), getOffsetY(), getScaledRadius(), mPaint);
        //阴影默认不画,暂时不看了
        drawShadow(canvas);
    }

5.2.drawingDelegated

    private CellLayout mDrawingDelegate;
    
    boolean drawingDelegated() {
        return mDrawingDelegate != null;
    }

5.3.drawBackgroundStroke

    public void drawBackgroundStroke(Canvas canvas) {
        if (!DRAW_STROKE) {//默认是false的
            return;
        }
        mPaint.setColor(setColorAlphaBound(mStrokeColor, mStrokeAlpha));
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(mStrokeWidth);

        float inset = 1f;
        getShape().drawShape(canvas,
                getOffsetX() + inset, getOffsetY() + inset, getScaledRadius() - inset, mPaint);
    }

5.4.setup

  • availableSpaceX 就是FolderIcon的测量宽度
  • topPadding 就是FolderIcon容器的padding
    public void setup(Context context, ActivityContext activity, View invalidateDelegate,
                      int availableSpaceX, int topPadding) {
        mInvalidateDelegate = invalidateDelegate;

        TypedArray ta = context.getTheme().obtainStyledAttributes(R.styleable.FolderIconPreview);
        mDotColor = ta.getColor(R.styleable.FolderIconPreview_folderDotColor, 0);
        mStrokeColor = ta.getColor(R.styleable.FolderIconPreview_folderIconBorderColor, 0);
        mBgColor = ta.getColor(R.styleable.FolderIconPreview_folderPreviewColor, 0);
        ta.recycle();

        DeviceProfile grid = activity.getDeviceProfile();
        //读取size
        previewSize = grid.folderIconSizePx;
        //容器宽减去预览图标的大小,除以2,为了居中显示
        basePreviewOffsetX = (availableSpaceX - previewSize) / 2;
        //默认的padding加上预览图的偏移量
        basePreviewOffsetY = topPadding + grid.folderIconOffsetYPx;

        // Stroke width is 1dp
        mStrokeWidth = context.getResources().getDisplayMetrics().density;

        if (DRAW_SHADOW) {
            float radius = getScaledRadius();
            float shadowRadius = radius + mStrokeWidth;
            int shadowColor = Color.argb(SHADOW_OPACITY, 0, 0, 0);
            mShadowShader = new RadialGradient(0, 0, 1,
                    new int[]{shadowColor, Color.TRANSPARENT},
                    new float[]{radius / shadowRadius, 1},
                    Shader.TileMode.CLAMP);
        }

        invalidate();//见补充1,
    }

>1.invalidate

    void invalidate() {
        if (mInvalidateDelegate != null) {
            mInvalidateDelegate.invalidate();
        }

        if (mDrawingDelegate != null) {
            mDrawingDelegate.invalidate();
        }
    }

5.5.drawLeaveBehind

这个就是FolderIcon点击展开Foler以后,自己显示的效果,可以看到,缩放了一半

    public void drawLeaveBehind(Canvas canvas) {
        float originalScale = mScale;
        mScale = 0.5f;//缩放一半

        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(Color.argb(160, 245, 245, 245));
        getShape().drawShape(canvas, getOffsetX(), getOffsetY(), getScaledRadius(), mPaint);

        mScale = originalScale;//数据恢复原本的
    }

>1.getScaledRadius

    //原本的半径是预览大小的一半
    public int getRadius() {
        return previewSize / 2;
    }
    //缩放后的半径
    int getScaledRadius() {
        return (int) (mScale * getRadius());
    }

>2.getOffsetX/Y

  • basePreviewOffsetX,原本的x方向偏移量
  • 返回的是缩放后的图片偏移量
    int getOffsetX() {
        return basePreviewOffsetX - (getScaledRadius() - getRadius());
    }

    int getOffsetY() {
        return basePreviewOffsetY - (getScaledRadius() - getRadius());
    }

6.IconShape

6.1.getShape

  • 我开始以为这个实例就是Circle了,可我们看到的明明是圆角矩形。
    private static IconShape sInstance = new Circle();
    private static float sNormalizationScale = ICON_VISIBLE_AREA_FACTOR;//0.92f

    public static IconShape getShape() {
        return sInstance;
    }

>1.init

这个静态方法里会选择最合适的shape,也就是会修改上边的sInstance的值,最终选择的是圆角矩形

    public static void init(Context context) {
        pickBestShape(context);//见6.2
    }

6.2.pickBestShape

  • AdaptiveIconDrawable就是差不多的圆角矩形,所以这里最终取的也是RoundedSquare
   protected static void pickBestShape(Context context) {
        // Pick any large size
        final int size = 200;

        Region full = new Region(0, 0, size, size);
        Region iconR = new Region();
        //自适应的图片,以这个图片占用的区域作为参考
        AdaptiveIconDrawable drawable = new AdaptiveIconDrawable(
                new ColorDrawable(Color.BLACK), new ColorDrawable(Color.BLACK));
        drawable.setBounds(0, 0, size, size);
        //设置路径,就是上边的AdaptiveIconDrawable,这个region和下边所有的shape进行对比
        iconR.setPath(drawable.getIconMask(), full);

        Path shapePath = new Path();
        Region shapeR = new Region();

        // Find the shape with minimum area of divergent region.
        int minArea = Integer.MAX_VALUE;
        IconShape closestShape = null;
        for (IconShape shape : getAllShapes(context)) {//见6.3
            shapePath.reset();
            shape.addToPath(shapePath, 0, 0, size / 2f);
            shapeR.setPath(shapePath, full);
            //循环所有的shpae和上边设置的iconR取差集
            shapeR.op(iconR, Op.XOR);
//看谁和iconR的差距最小就用谁。
            int area = GraphicsUtils.getArea(shapeR);
            if (area < minArea) {
                minArea = area;
                closestShape = shape;
            }
        }

        if (closestShape != null) {
            sInstance = closestShape;
        }

        // Initialize shape properties,打印了下是0.8094864
        sNormalizationScale = IconNormalizer.normalizeAdaptiveIcon(drawable, size, null);
    }

6.3.getAllShapes

    private static List<IconShape> getAllShapes(Context context) {
        ArrayList<IconShape> result = new ArrayList<>();
        //配置文件见补充1
        try (XmlResourceParser parser = context.getResources().getXml(R.xml.folder_shapes)) {

            // Find the root tag
            int type;
            while ((type = parser.next()) != XmlPullParser.END_TAG
                    && type != XmlPullParser.END_DOCUMENT
                    && !"shapes".equals(parser.getName()));

            final int depth = parser.getDepth();
            int[] radiusAttr = new int[] {R.attr.folderIconRadius};

            while (((type = parser.next()) != XmlPullParser.END_TAG ||
                    parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

                if (type == XmlPullParser.START_TAG) {
                    AttributeSet attrs = Xml.asAttributeSet(parser);
                    TypedArray a = context.obtainStyledAttributes(attrs, radiusAttr);
                    IconShape shape = getShapeDefinition(parser.getName(), a.getFloat(0, 1));
                    a.recycle();

                    result.add(shape);
                }
            }
        } catch (IOException | XmlPullParserException e) {
            throw new RuntimeException(e);
        }
        return result;
    }

>1.folder_shapes.xml

  • 参数表示shape类型,以及圆角半径的百分比(后续和提供的半径相乘)
<shapes xmlns:launcher="http://schemas.android.com/apk/res-auto" >

    <Circle launcher:folderIconRadius="1" />

    <!-- Default icon for AOSP ,打印了下,官方源码里用的是这个-->
    <RoundedSquare launcher:folderIconRadius="0.16" />

    <!-- Rounded icon from RRO -->
    <RoundedSquare launcher:folderIconRadius="0.6" />

    <!-- Square icon -->
    <RoundedSquare launcher:folderIconRadius="0" />

    <TearDrop launcher:folderIconRadius="0.3" />
    <Squircle launcher:folderIconRadius="0.2" />

</shapes>

>2.getShapeDefinition

    private static IconShape getShapeDefinition(String type, float radius) {
        switch (type) {
            case "Circle":
                return new Circle();
            case "RoundedSquare":
                return new RoundedSquare(radius);
            case "TearDrop":
                return new TearDrop(radius);
            case "Squircle":
                return new Squircle(radius);
            default:
                throw new IllegalArgumentException("Invalid shape type: " + type);
        }
    }

6.4.RoundedSquare

    public static class RoundedSquare extends SimpleRectShape {

>1.drawShape

可以看到画的就是个圆角矩形

        public void drawShape(Canvas canvas, float offsetX, float offsetY, float radius, Paint p) {
            float cx = radius + offsetX;
            float cy = radius + offsetY;
            float cr = radius * mRadiusRatio;
            canvas.drawRoundRect(cx - radius, cy - radius, cx + radius, cy + radius, cr, cr, p);
        }

>2.addToPath

添加的路径也是个圆角矩形

        public void addToPath(Path path, float offsetX, float offsetY, float radius) {
            float cx = radius + offsetX;
            float cy = radius + offsetY;
            float cr = radius * mRadiusRatio;
            path.addRoundRect(cx - radius, cy - radius, cx + radius, cy + radius, cr, cr,
                    Path.Direction.CW);
        }