基于Android7.0的Launcher3源码分析(3)——显示相关参数的初始化

  前面第一篇讲解了Launcher从初始化到显示的大概流程。接下来的文章将会一步步详细分析这些流程。今天这篇文章讲下Launcher显示相关参数的初始化和处理过程。

初始化入口

  从onCreate()进入,首先判断了Launcher的排列方向,一般都是纵向的,所以本文就以纵向为例。通过app.getInvariantDeviceProfile().portraitProfile 获取到mDeviceProfile的实例对象,该对象中以成员变量的形式存储了显示相关参数。然后,根据这些参数,通过 mDeviceProfile.layout() 对Launcher的控件进行布局的调整。Launcher主要有哪些控件组成,详见文章 Launcher3源码分析(2)——UI显示控件的组成
  下面围绕两个方面展开。1、mDeviceProfile 对象如何初始化;2、如何根据 mDeviceProfile 对象中保存的数据调整布局参数。

public class Launcher extends Activity
        implements View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks,
                   View.OnTouchListener, PageSwitchListener, LauncherProviderChangeListener {
    ......

    private DeviceProfile mDeviceProfile;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ......
        LauncherAppState app = LauncherAppState.getInstance();

        // Load configuration-specific DeviceProfile
        mDeviceProfile = getResources().getConfiguration().orientation
                == Configuration.ORIENTATION_LANDSCAPE ?
                app.getInvariantDeviceProfile().landscapeProfile
                : app.getInvariantDeviceProfile().portraitProfile;
        ......
        setContentView(R.layout.launcher);
        setupViews();

        mDeviceProfile.layout(this);

        ......
    }
}

1、mDeviceProfile 对象初始化

1.1 LauncherAppState初始化

  可以看到,这里获取了LauncherAppState的一个单例对象,看下它的初始化。这里最重要的是成员对象 mInvariantDeviceProfile 的初始化,因为 app.getInvariantDeviceProfile() 就是获取的该对象。

public class LauncherAppState {

    ......

    private static LauncherAppState INSTANCE;
    private InvariantDeviceProfile mInvariantDeviceProfile;

    public static LauncherAppState getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new LauncherAppState();
        }
        return INSTANCE;
    }

    ......

    private LauncherAppState() {
        ......

        mInvariantDeviceProfile = new InvariantDeviceProfile(sContext);
        mIconCache = new IconCache(sContext, mInvariantDeviceProfile);
        mWidgetCache = new WidgetPreviewLoader(sContext, mIconCache);

        mAppFilter = AppFilter.loadByName(sContext.getString(R.string.app_filter_class));
        mModel = new LauncherModel(this, mIconCache, mAppFilter);

        ......
    }

    public InvariantDeviceProfile getInvariantDeviceProfile() {
        return mInvariantDeviceProfile;
    }
    ......
}

1.2 InvariantDeviceProfile初始化

  来到InvariantDeviceProfile的单个参数的构造方法。可以看到,之前在Launcher中app.getInvariantDeviceProfile().portraitProfile 的 portraitProfile 就是在这里初始化的。下面一步步来看。

public class InvariantDeviceProfile {
    ......

    DeviceProfile portraitProfile;

    @TargetApi(23)
    InvariantDeviceProfile(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        Display display = wm.getDefaultDisplay();
        DisplayMetrics dm = new DisplayMetrics();
        display.getMetrics(dm);

        Point smallestSize = new Point();
        Point largestSize = new Point();
        display.getCurrentSizeRange(smallestSize, largestSize); //获取屏幕的最大最小尺寸

        // This guarantees that width < height
        minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y), dm); //px转dp
        minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y), dm); //px转dp

        ArrayList<InvariantDeviceProfile> closestProfiles =
                findClosestDeviceProfiles(minWidthDps, minHeightDps, getPredefinedDeviceProfiles());
        InvariantDeviceProfile interpolatedDeviceProfileOut =
                invDistWeightedInterpolate(minWidthDps,  minHeightDps, closestProfiles);

        InvariantDeviceProfile closestProfile = closestProfiles.get(0);
        numRows = closestProfile.numRows;
        numColumns = closestProfile.numColumns;
        numHotseatIcons = closestProfile.numHotseatIcons;
        hotseatAllAppsRank = (int) (numHotseatIcons / 2);
        defaultLayoutId = closestProfile.defaultLayoutId;
        numFolderRows = closestProfile.numFolderRows;
        numFolderColumns = closestProfile.numFolderColumns;
        minAllAppsPredictionColumns = closestProfile.minAllAppsPredictionColumns;

        iconSize = interpolatedDeviceProfileOut.iconSize; //初始图标大小
        iconBitmapSize = Utilities.pxFromDp(iconSize, dm);
        iconTextSize = interpolatedDeviceProfileOut.iconTextSize; //初始文字大小
        hotseatIconSize = interpolatedDeviceProfileOut.hotseatIconSize;
        fillResIconDpi = getLauncherIconDensity(iconBitmapSize);

        // If the partner customization apk contains any grid overrides, apply them
        // Supported overrides: numRows, numColumns, iconSize
        applyPartnerDeviceProfileOverrides(context, dm);

        Point realSize = new Point();
        display.getRealSize(realSize); //获取屏幕实际的尺寸
        // The real size never changes. smallSide and largeSide will remain the
        // same in any orientation.
        int smallSide = Math.min(realSize.x, realSize.y);
        int largeSide = Math.max(realSize.x, realSize.y);

        landscapeProfile = new DeviceProfile(context, this, smallestSize, largestSize,
                largeSide, smallSide, true /* isLandscape */);
        portraitProfile = new DeviceProfile(context, this, smallestSize, largestSize,
                smallSide, largeSide, false /* isLandscape */);
    }
1.2.1 获取屏幕的最大和最小尺寸

  通过getCurrentSizeRange方法返回应用程序可以操作的最大和最小尺寸范围,赋值给largestSize和smallestSize。
    largestSize.x:屏幕最大可用宽度; largestSize.y:屏幕最大可用高度
    smallestSize.x:屏幕最小可用宽度; smallestSize.y:屏幕最小可用高度
  什么是最大最小尺寸呢?我们知道,Android系统界面上会显示系统控件,如statusBar和navigationBar,当这些控件都显示时,可用的最小屏幕高度将会比屏幕的实际高度小,也就是可用最小范围。当statusBar这些系统控件都被隐藏时,就可能返回屏幕可用高度的最大值。
  上面说的largestSize.x等都是以px为单位的,为了更好的适配分辨率,需要将其转化为dp为单位。通过dpiFromPx方法将其进行转化,然后把屏幕可用宽度的dp值赋值给minWidthDps,将屏幕可用高度的dp值赋值给minHeightDps。下面会根据这两个值来确定一部分参数的值。

1.2.2 根据设备配置参数获得当前图标和文字大小

  可以看到,这里针对一些手机机型,进行了一些适配,定义了图标大小,默认的桌面行列布局等参数。并以ArrayList的形式进行存储,并返回给findClosestDeviceProfiles方法当做传入的参数。

    ArrayList<InvariantDeviceProfile> getPredefinedDeviceProfiles() {
        ArrayList<InvariantDeviceProfile> predefinedDeviceProfiles = new ArrayList<>();
        // width, height, #rows, #columns, #folder rows, #folder columns,
        // iconSize, iconTextSize, #hotseat, #hotseatIconSize, defaultLayoutId.
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Super Short Stubby",
                255, 300,     2, 3, 2, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_3x3));
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Shorter Stubby",
                255, 400,     3, 3, 3, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_3x3));
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Short Stubby",
                275, 420,     3, 4, 3, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Stubby",
                255, 450,     3, 4, 3, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus S",
                296, 491.33f, 4, 4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 4",
                359, 567,     4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4));
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 5",
                335, 567,     4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4));
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Large Phone",
                406, 694,     5, 5, 4, 4, 4, 64, 14.4f,  5, 56, R.xml.default_workspace_5x5));
        // The tablet profile is odd in that the landscape orientation
        // also includes the nav bar on the side
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 7",
                575, 904,     5, 6, 4, 5, 4, 72, 14.4f,  7, 60, R.xml.default_workspace_5x6));
        // Larger tablet profiles always have system bars on the top & bottom
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 10",
                727, 1207,    5, 6, 4, 5, 4, 76, 14.4f,  7, 76, R.xml.default_workspace_5x6));
        predefinedDeviceProfiles.add(new InvariantDeviceProfile("20-inch Tablet",
                1527, 2527,   7, 7, 6, 6, 4, 100, 20,  7, 72, R.xml.default_workspace_5x6));
        return predefinedDeviceProfiles;
    }

  来到findClosestDeviceProfiles方法。该方法根据之前获取的屏幕可用宽高,来把getPredefinedDeviceProfiles获取到的不同机型的参数进行排序。这里采取一个升序的形式排序,即与屏幕可用宽高相差越小的机型配置排在越前面。这样返回的pointsByNearness链表中,排在第一个的就是与当前屏幕可用宽高最为接近的一组配置。

    ArrayList<InvariantDeviceProfile> findClosestDeviceProfiles(
            final float width, final float height, ArrayList<InvariantDeviceProfile> points) {

        // Sort the profiles by their closeness to the dimensions
        ArrayList<InvariantDeviceProfile> pointsByNearness = points;
        Collections.sort(pointsByNearness, new Comparator<InvariantDeviceProfile>() {
            public int compare(InvariantDeviceProfile a, InvariantDeviceProfile b) {
                return Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps),
                        dist(width, height, b.minWidthDps, b.minHeightDps));
            }
        });

        return pointsByNearness;
    }

  来到invDistWeightedInterpolate方法。拿到排在第一组的机型参数,如果与当前屏幕可用宽高完全吻合,即直接返回该组机型的参数。如果没有完全吻合,则经过一系列的计算,来得到与当前屏幕可用宽高最为合适的各参数大小。具体算法这边就不细究了。

    InvariantDeviceProfile invDistWeightedInterpolate(float width, float height,
                ArrayList<InvariantDeviceProfile> points) {
        float weights = 0;

        InvariantDeviceProfile p = points.get(0);
        if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) {
            return p;
        }

        InvariantDeviceProfile out = new InvariantDeviceProfile();
        for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) {
            p = new InvariantDeviceProfile(points.get(i));
            float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER);
            weights += w;
            out.add(p.multiply(w));
        }
        return out.multiply(1.0f/weights);
    }

  计算完毕后,对重要参数进行赋值,留待使用。这里的iconSize等参数只是一个基础大小,并不是最终大小,后面可能根据需要还会有一些处理。

        iconSize = interpolatedDeviceProfileOut.iconSize;
        iconBitmapSize = Utilities.pxFromDp(iconSize, dm);
        iconTextSize = interpolatedDeviceProfileOut.iconTextSize;
        hotseatIconSize = interpolatedDeviceProfileOut.hotseatIconSize;
1.2.3 获取屏幕的实际尺寸

  通过getRealSize返回屏幕可用大小的实际尺寸。

  为了方便理解,这里以屏幕物理大小为 720*1280 的手机为例,得到的输出参数如下:
  - 最小可用尺寸:smallestSize.x = 720 smallestSize.y = 670
  - 最大可用尺寸:largestSize.x = 1196 largestSize.y = 1134
  - 实际尺寸:realSize.x = 720 realSize.y = 1280

1.2.4 完成实例化

  将InvariantDeviceProfile对象, smallestSize, largestSize, smallSide, largeSide作为参数传入,就到了DeviceProfile对象的实例化的最后一步。

    public DeviceProfile(Context context, InvariantDeviceProfile inv,
            Point minSize, Point maxSize,
            int width, int height, boolean isLandscape) {

        this.inv = inv;
        this.isLandscape = isLandscape;

        Resources res = context.getResources();
        DisplayMetrics dm = res.getDisplayMetrics();

        // Constants from resources
        isTablet = res.getBoolean(R.bool.is_tablet);
        isLargeTablet = res.getBoolean(R.bool.is_large_tablet);
        isPhone = !isTablet && !isLargeTablet;

        // Some more constants
        transposeLayoutWithOrientation =
                res.getBoolean(R.bool.hotseat_transpose_layout_with_orientation);

        ComponentName cn = new ComponentName(context.getPackageName(),
                this.getClass().getName());
        defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null);
        edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
        desiredWorkspaceLeftRightMarginPx = 2 * edgeMarginPx;
        pageIndicatorHeightPx =
                res.getDimensionPixelSize(R.dimen.dynamic_grid_page_indicator_height);
        defaultPageSpacingPx =
                res.getDimensionPixelSize(R.dimen.dynamic_grid_workspace_page_spacing);
        overviewModeMinIconZoneHeightPx =
                res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_min_icon_zone_height);
        overviewModeMaxIconZoneHeightPx =
                res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_max_icon_zone_height);
        overviewModeBarItemWidthPx =
                res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_bar_item_width);
        overviewModeBarSpacerWidthPx =
                res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_bar_spacer_width);
        overviewModeIconZoneRatio =
                res.getInteger(R.integer.config_dynamic_grid_overview_icon_zone_percentage) / 100f;
        iconDrawablePaddingOriginalPx =
                res.getDimensionPixelSize(R.dimen.dynamic_grid_icon_drawable_padding);

        // AllApps uses the original non-scaled icon text size
        allAppsIconTextSizeSp = inv.iconTextSize;

        // AllApps uses the original non-scaled icon size
        allAppsIconSizePx = Utilities.pxFromDp(inv.iconSize, dm);

        // Determine sizes.
        widthPx = width;
        heightPx = height;
        if (isLandscape) {
            availableWidthPx = maxSize.x;
            availableHeightPx = minSize.y;
        } else {
            availableWidthPx = minSize.x;
            availableHeightPx = maxSize.y;
        }

        // Calculate the remaining vars
        updateAvailableDimensions(dm, res);
        computeAllAppsButtonSize(context);
    }

  构造方法进行了参数最终的赋值过程。将之前保存了图标基础大小等参数的InvariantDeviceProfile对象,赋给成员对象 this.inv = inv; 留待后面取用。如:allAppsIconTextSizeSp 就是直接取的inv对象中的数值进行赋值。

allAppsIconTextSizeSp = inv.iconTextSize

  后面就是通过updateAvailableDimensions(dm, res)和computeAllAppsButtonSize(context)进行一些赋值操作,如下,就不细说了。

iconSizePx = (int) (Utilities.pxFromDp(inv.iconSize, dm) * scale);
iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, dm) * scale);

  初始化完成后,本文最开始通过app.getInvariantDeviceProfile().portraitProfile返回的就是该实例对象。 Controller 层就可以通过该对象获取到显示的相关参数。

2、根据 mDeviceProfile 对象中保存的数据调整布局参数

  通过传入Launcher的实例,来获取显示控件的实例对象,最后通过修改对应的 LayoutParams 进行布局的调整。
  可以看到,这里对SearchDropTargetBar、Workspace、Hotseat、PageIndicator和长按桌面显示的OverviewPanel都进行了布局调整。这些控件的介绍详见 Launcher3源码分析(2)——UI显示控件的组成

public class DeviceProfile {
    ......

    public void layout(Launcher launcher) {
        FrameLayout.LayoutParams lp;
        boolean hasVerticalBarLayout = isVerticalBarLayout();
        final boolean isLayoutRtl = Utilities.isRtl(launcher.getResources());

        // Layout the search bar space
        Rect searchBarBounds = getSearchBarBounds(isLayoutRtl);
        View searchBar = launcher.getSearchDropTargetBar();
        lp = (FrameLayout.LayoutParams) searchBar.getLayoutParams();
        lp.width = searchBarBounds.width();
        lp.height = searchBarBounds.height();
        lp.topMargin = searchBarTopExtraPaddingPx;
        if (hasVerticalBarLayout) {
            lp.gravity = Gravity.LEFT;

            LinearLayout targets = (LinearLayout) searchBar.findViewById(R.id.drag_target_bar);
            targets.setOrientation(LinearLayout.VERTICAL);
            FrameLayout.LayoutParams targetsLp = (FrameLayout.LayoutParams) targets.getLayoutParams();
            targetsLp.gravity = Gravity.TOP;
            targetsLp.height = LayoutParams.WRAP_CONTENT;

        } else {
            // Horizontal search bar space
            lp.gravity = Gravity.TOP|Gravity.CENTER_HORIZONTAL;
        }
        searchBar.setLayoutParams(lp);

        // Layout the workspace
        PagedView workspace = (PagedView) launcher.findViewById(R.id.workspace);
        lp = (FrameLayout.LayoutParams) workspace.getLayoutParams();
        lp.gravity = Gravity.CENTER;
        Rect padding = getWorkspacePadding(isLayoutRtl);
        workspace.setLayoutParams(lp);
        workspace.setPadding(padding.left, padding.top, padding.right, padding.bottom);
        workspace.setPageSpacing(getWorkspacePageSpacing(isLayoutRtl));

        // Layout the hotseat
        View hotseat = launcher.findViewById(R.id.hotseat);
        lp = (FrameLayout.LayoutParams) hotseat.getLayoutParams();
        float workspaceCellWidth = (float) getCurrentWidth() / inv.numColumns;
        float hotseatCellWidth = (float) getCurrentWidth() / inv.numHotseatIcons;
        int hotseatAdjustment = Math.round((workspaceCellWidth - hotseatCellWidth) / 2);
        if (hasVerticalBarLayout) {
            // Vertical hotseat -- The hotseat is fixed in the layout to be on the right of the
            //                     screen regardless of RTL
            lp.gravity = Gravity.RIGHT;
            lp.width = normalHotseatBarHeightPx;
            lp.height = LayoutParams.MATCH_PARENT;
            hotseat.findViewById(R.id.layout).setPadding(0, 2 * edgeMarginPx, 0, 2 * edgeMarginPx);
        } else if (isTablet) {
            // Pad the hotseat with the workspace padding calculated above
            lp.gravity = Gravity.BOTTOM;
            lp.width = LayoutParams.MATCH_PARENT;
            lp.height = hotseatBarHeightPx;
            hotseat.findViewById(R.id.layout).setPadding(
                    hotseatAdjustment + padding.left, 0,
                    hotseatAdjustment + padding.right, 2 * edgeMarginPx);
        } else {
            // For phones, layout the hotseat without any bottom margin
            // to ensure that we have space for the folders
            lp.gravity = Gravity.BOTTOM;
            lp.width = LayoutParams.MATCH_PARENT;
            lp.height = hotseatBarHeightPx;
            hotseat.findViewById(R.id.layout).setPadding(
                    hotseatAdjustment + padding.left, 0,
                    hotseatAdjustment + padding.right, 0);
        }
        hotseat.setLayoutParams(lp);

        // Layout the page indicators
        View pageIndicator = launcher.findViewById(R.id.page_indicator);
        if (pageIndicator != null) {
            if (hasVerticalBarLayout) {
                // Hide the page indicators when we have vertical search/hotseat
                pageIndicator.setVisibility(View.GONE);
            } else {
                // Put the page indicators above the hotseat
                lp = (FrameLayout.LayoutParams) pageIndicator.getLayoutParams();
                lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
                lp.width = LayoutParams.WRAP_CONTENT;
                lp.height = LayoutParams.WRAP_CONTENT;
                lp.bottomMargin = hotseatBarHeightPx;
                pageIndicator.setLayoutParams(lp);
            }
        }

        // Layout the Overview Mode
        ViewGroup overviewMode = launcher.getOverviewPanel();
        if (overviewMode != null) {
            int overviewButtonBarHeight = getOverviewModeButtonBarHeight();
            lp = (FrameLayout.LayoutParams) overviewMode.getLayoutParams();
            lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;

            int visibleChildCount = getVisibleChildCount(overviewMode);
            int totalItemWidth = visibleChildCount * overviewModeBarItemWidthPx;
            int maxWidth = totalItemWidth + (visibleChildCount-1) * overviewModeBarSpacerWidthPx;

            lp.width = Math.min(availableWidthPx, maxWidth);
            lp.height = overviewButtonBarHeight;
            overviewMode.setLayoutParams(lp);

            if (lp.width > totalItemWidth && visibleChildCount > 1) {
                // We have enough space. Lets add some margin too.
                int margin = (lp.width - totalItemWidth) / (visibleChildCount-1);
                View lastChild = null;

                // Set margin of all visible children except the last visible child
                for (int i = 0; i < visibleChildCount; i++) {
                    if (lastChild != null) {
                        MarginLayoutParams clp = (MarginLayoutParams) lastChild.getLayoutParams();
                        if (isLayoutRtl) {
                            clp.leftMargin = margin;
                        } else {
                            clp.rightMargin = margin;
                        }
                        lastChild.setLayoutParams(clp);
                        lastChild = null;
                    }
                    View thisChild = overviewMode.getChildAt(i);
                    if (thisChild.getVisibility() != View.GONE) {
                        lastChild = thisChild;
                    }
                }
            }
        }
    }
}

扩展

这边列举了初始化的一些显示相关参数。

    public int numRows; //桌面每行图标数
    public int numColumns; //桌面每列图标数
    // Device properties in current orientation
    public final int availableWidthPx; //屏幕可用宽度
    public final int availableHeightPx; //屏幕可用高度
    // Workspace
    private final int pageIndicatorHeightPx; //指示器高度
    // Workspace icons
    public int iconSizePx; //图标大小
    public int iconTextSizePx; //图标文字大小
    // Folder
    public int folderIconSizePx;
    public int folderCellWidthPx;
    public int folderCellHeightPx;
    // Hotseat
    public int hotseatCellWidthPx;
    public int hotseatCellHeightPx;
    public int hotseatIconSizePx;
    // All apps
    public int allAppsButtonVisualSize; //allApp入口按钮大小
    public final int allAppsIconSizePx;
    public final float allAppsIconTextSizeSp;

如果想修改图标默认大小。一般有两种方式,

  1. 修改InvariantDeviceProfile对象中图标原始基础大小:成员变量 iconSize
  2. 修改DeviceProfile对象中最终生效的图标大小:成员变量 iconSizePx
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值