ColorStateList 使用详解

1. 是什么?

ColorStateList(颜色状态列表)是一个可以定义在 XML 布局文件中,并最终根据 ColorStateList 应用的 View 的状态显示不同颜色的对象。

A ColorStateList is an object you can define in XML that you can apply as a color, but will actually change colors, depending on the state of the View object to which it is applied.

最终效果如下:

界面中两按钮文字的颜色随着按钮的状态而改变。

2. 怎么用?

从 ColorStateList 的定义可以知道,创建 ColorStateList 的方式应该不止有一种。接下来,我们就尝试从两方面创建 ColorStateList:

  1. XML
  2. Java 代码
2.1 如何在 XML 中定义 ColorStateList
2.1.1 文件位置
res/color/filename.xml
2.1.2 编译之后的数据类型
ColorStateList
2.1.3 应用方式
  1. In Java: R.color.filename
  2. In XML: @[package:]color/filename
2.1.4 语法
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item
        android:color="hex_color"
        android:state_pressed=["true" | "false"]
        android:state_focused=["true" | "false"]
        android:state_selected=["true" | "false"]
        android:state_checkable=["true" | "false"]
        android:state_checked=["true" | "false"]
        android:state_enabled=["true" | "false"]
        android:state_window_focused=["true" | "false"] />
</selector>
2.1.5 属性解析
属性定义取值范围
color不同状态的颜色值十六进制的颜色值。
可以是如下格式:
#RGB
#ARGB
#RRGGBB
#AARRGGBB
state_pressedView 按下的状态true,false。
true,按下;
false,默认状态,即没有按下之前的状态。
state_selectedView 选中的状态true,false。
true,选中;
false,未选中。

其他的属性类似,在此就不做赘述了。想要了解更多关于 state_xxx
的内容,请查看Color state list resource

2.1.6 示例
//1. text_color_state_list.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/green_700" android:state_pressed="true" />
    <item android:color="@color/grey_700" android:state_pressed="false" />
    <!--默认项-->
    <item android:color="@color/grey_700" />
</selector>
//2. 在 XML 布局文件中应用 text_color_state_list
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/alphabet_a"
        android:layout_width="@dimen/avatar_size"
        android:layout_height="@dimen/padding_seventy_two"
        android:text="@string/alphabet_a"
        android:textColor="@color/text_color_state_list"
        android:textSize="@dimen/font_thirty_two" />
        
</LinearLayout>

最终效果如下:

//3. 在 Java 代码中使用 text_color_state_list
public class MainActivity extends AppCompatActivity {

    private Button  mAlphaB;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    private void initView(){
        mAlphaB = findViewById(R.id.alphabet_b);

        Resources resources = getResources();
        ColorStateList colorStateList = resources.getColorStateList(R.color.text_color_state_list);
        mAlphaB.setTextColor(colorStateList);
    }

}

在 Java 中使用在 XML 中定义的 ColorStateList 的效果与在 XML 中使用在 XML 中定义的 ColorStateList 的效果一样,所以就不赘述了。

2.1.7 注意事项
2.1.7.1 ColorStateList 中定义的默认 Item 一定要放在最下面

ColorStateList 中定义的默认 Item 一定要放在最下面,否则后面的 Item 将被忽略,Android Framework 在此处选择资源的时候,并不是按照“最优选项”选择的,而是按照从上到下选择第一个匹配的。

Remember that the first item in the state list that matches the current state of the object will be applied. So if the first item in the list contains none of the state attributes above, then it will be applied every time, which is why your default value should always be last, as demonstrated in the following example.

举个例子:

  1. 默认 Item 放在最下面:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/green_700" android:state_pressed="true" />
    <item android:color="@color/grey_700" android:state_pressed="false" />
    <!--默认项-->
    <item android:color="@color/grey_700" />
</selector>

最终效果如下:

  1. 默认 Item 放在最上面:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!--默认项-->
    <item android:color="@color/grey_700" />
    <item android:color="@color/green_700" android:state_pressed="true" />
    <item android:color="@color/grey_700" android:state_pressed="false" />
</selector>

最终效果如下:

由上面的运行效果可知:当默认的 Item 在最上面的时候,Button 的文字颜色并不会随着 Button 状态的改变而改变。因此在后面定义 ColorStateList 的时候,如果想要应用 ColorStateList 的 View 内容(字体或者其他)的颜色随着 View 的状态而改变,就需要把 ColorStateList 中默认的 Item 定义在最下面。

2.1.7.2 ColorStateList 是不能用于 View 的 Background
//1. View 部分源码  
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
    
    ...
    
    public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        this(context);

        final TypedArray a = context.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
        
        ...
        
        for (int i = 0; i < N; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case com.android.internal.R.styleable.View_background:
                    background = a.getDrawable(attr);
                    break;
                    
                    ...
                    
                }
            
            ...
            
        }
        
        ...
        
    }
    
    ...
    
}

由 View 源码可知:View 的 Background 最终是通过 TypedArray 的 GetDrawable 方法获取的。

//2. TypedArray 部分源码  
public class TypedArray {
    
    ...
    
    /**
     * Retrieve the Drawable for the attribute at <var>index</var>.
     * <p>
     * This method will throw an exception if the attribute is defined but is
     * not a color or drawable resource.
     *
     * @param index Index of attribute to retrieve.
     *
     * @return Drawable for the attribute, or {@code null} if not defined.
     * @throws RuntimeException if the TypedArray has already been recycled.
     * @throws UnsupportedOperationException if the attribute is defined but is
     *         not a color or drawable resource.
     */
    @Nullable
    public Drawable getDrawable(@StyleableRes int index) {
        return getDrawableForDensity(index, 0);
    }
    
    ...
    
}

由 TypedArray 源码可知,在 TypedArray 的 GetDrawable 中只能接收纯 Color 或者 Drawable Resource,而 ColorStateList 并未在此范围内,因此 ColorStateList 是不能用于 View 的 Background(如果在 View 的 Background 中引用 ColorStateList,应用程序将会 Crash)。

throws UnsupportedOperationException if the attribute is defined but is not a color or drawable resource.

2.1.7.2 StateListDrawable 是不能用于 TextView 系的 TextColor
//1. TextView 部分源码  
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
    ...
    
    public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        
        ...
        
        readTextAppearance(context, a, attributes, true /* styleArray */);
        
        ...
    }
    
    ...
}
//2. readTextAppearance 方法  
private void readTextAppearance(Context context, TypedArray appearance, TextAppearanceAttributes attributes, boolean styleArray) {
        
        ...
        
        for (int i = 0; i < n; i++) {
            
            ...
            
            switch (index) {
                case com.android.internal.R.styleable.TextAppearance_textColorHighlight:
                    attributes.mTextColorHighlight = appearance.getColor(attr, attributes.mTextColorHighlight);
                    break;
                
                ...
                
            }
            
            ...
            
        }
        
        ...
        
}

通过 TextView 源码可知,TextView 的 TextColor 最终是通过 TypedArray 的 GetColor 方法获取的。

//3. TypedArray 部分源码  
public class TypedArray {
    
    ...
    
    /**
     * Retrieve the color value for the attribute at <var>index</var>.  If
     * the attribute references a color resource holding a complex
     * {@link android.content.res.ColorStateList}, then the default color from
     * the set is returned.
     * <p>
     * This method will throw an exception if the attribute is defined but is
     * not an integer color or color state list.
     *
     * @param index Index of attribute to retrieve.
     * @param defValue Value to return if the attribute is not defined or
     *                 not a resource.
     *
     * @return Attribute color value, or defValue if not defined.
     * @throws RuntimeException if the TypedArray has already been recycled.
     * @throws UnsupportedOperationException if the attribute is defined but is
     *         not an integer color or color state list.
     */
    @ColorInt
    public int getColor(@StyleableRes int index, @ColorInt int defValue) {
        if (mRecycled) {
            throw new RuntimeException("Cannot make calls to a recycled instance!");
        }

        final int attrIndex = index;
        index *= STYLE_NUM_ENTRIES;

        final int[] data = mData;
        final int type = data[index + STYLE_TYPE];
        if (type == TypedValue.TYPE_NULL) {
            return defValue;
        } else if (type >= TypedValue.TYPE_FIRST_INT
                && type <= TypedValue.TYPE_LAST_INT) {
            return data[index + STYLE_DATA];
        } else if (type == TypedValue.TYPE_STRING) {
            final TypedValue value = mValue;
            if (getValueAt(index, value)) {
                final ColorStateList csl = mResources.loadColorStateList(
                        value, value.resourceId, mTheme);
                return csl.getDefaultColor();
            }
            return defValue;
        } else if (type == TypedValue.TYPE_ATTRIBUTE) {
            final TypedValue value = mValue;
            getValueAt(index, value);
            throw new UnsupportedOperationException(
                    "Failed to resolve attribute at index " + attrIndex + ": " + value);
        }

        throw new UnsupportedOperationException("Can't convert value at index " + attrIndex
                + " to color: type=0x" + Integer.toHexString(type));
    }
    
    ...
    
}
    

由 TypedArray 源码可知,在 TypedArray 的 getColor 中只能接收纯 Color 或者 Color State List,而 StateListDrawable 并未在此范围内,因此 StateListDrawable 是不能用于 TextView 系的 TextColor(如果在 TextView 的 TextColor 中引用 StateListDrawable 程序将会出 Bug,但是不会 Crash)。

throws UnsupportedOperationException if the attribute is defined but is not an integer color or color state list.

2.2 如何在代码中定义 ColorStateList
2.2.1 ColorStateList 源码解析

ColorStateList 部分源码如下:

public class ColorStateList extends ComplexColor implements Parcelable {
    
    ...

    /**
     * Creates a ColorStateList that returns the specified mapping from
     * states to colors.
     */
    public ColorStateList(int[][] states, @ColorInt int[] colors) {
        mStateSpecs = states;
        mColors = colors;

        onColorsChanged();
    }

    ...

}

由上面的源码可知,在创建 ColorStateList 的时候,需要传入两个数组,第一个数组是存储状态值的,第二个数组是存储状态对应颜色值的。
简单对比一下 XML 中定义 ColorStateList 的语法,其实很容易就明白为什么在 ColorStateList 构造方法中存储状态值的数组是二维数组。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item
        android:color="hex_color"
        android:state_pressed=["true" | "false"]
        android:state_focused=["true" | "false"]
        android:state_selected=["true" | "false"]
        android:state_checkable=["true" | "false"]
        android:state_checked=["true" | "false"]
        android:state_enabled=["true" | "false"]
        android:state_window_focused=["true" | "false"] />
</selector>

因为在每一个 Item 中可以有很多个状态(state_xxx),每一个 Item 中的所有这些状态只对应一个颜色值。也就是说,ColorStateList 构造方法中的存储状态的数组的第一层数组的 Size 只要和存储状态对应颜色值的数组的 Size 一致就好了。

举个例子(伪代码):

//状态值(states 第一层 size 为 2)
int[][] states = new int[2][];
states[0] = new int[] {android.R.attr.state_xxx};
states[1] = new int[] {};
//不同状态对应的颜色值(colors size 为 2)
int[] colors = new int[] { R.color.pressed, R.color.normal};
ColorStateList colorList = new ColorStateList(states, colors);
2.2.2 示例
public class MainActivity extends AppCompatActivity {

    private Button  mAlphaB;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    private void initView(){
    
        mAlphaB = findViewById(R.id.alphabet_b);
        ColorStateList colorStateList = createColorStateList(getResources().getColor(R.color.green_700), getResources().getColor(R.color.grey_700));
        mAlphaB.setTextColor(colorStateList);
        
    }

    private ColorStateList createColorStateList(int pressed, int normal) {
        //状态
        int[][] states = new int[2][];
        //按下
        states[0] = new int[] {android.R.attr.state_pressed};
        //默认
        states[1] = new int[] {};
        
        //状态对应颜色值(按下,默认)
        int[] colors = new int[] { pressed, normal};
        ColorStateList colorList = new ColorStateList(states, colors);
        return colorList;
    }

}

最终效果如下:

2.2.3 自定义 ColorStateList

除了上面的方式之外,还可以继承 ColorStateList 实现自定义 ColorStateList,但由于 ColorStateList 可更改的属性太少,所以自定义 ColorStateList 并没有什么意义。

简单示例:

public class CustomColorStateList extends ColorStateList {

    public CustomColorStateList(int[][] states, int[] colors) {
        super(states, colors);
    }

}

具体使用方法同《2.2.2 示例》一样,所以再次不做赘述。

3. 工作原理

下面是在代码中使用在 XML 布局文件中创建的 ColorStateList 的方法:

//1. text_color_state_list.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/green_700" android:state_pressed="true" />
    <item android:color="@color/grey_700" android:state_pressed="false" />
    <!--默认项-->
    <item android:color="@color/grey_700" />
</selector>
//2. 在 Java 代码中使用 text_color_state_list
public class MainActivity extends AppCompatActivity {

    private Button  mAlphaB;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    private void initView(){
        mAlphaB = findViewById(R.id.alphabet_b);

        Resources resources = getResources();
        ColorStateList colorStateList = resources.getColorStateList(R.color.text_color_state_list);
        mAlphaB.setTextColor(colorStateList);
    }

}

既然是通过 Button 的 SetTextColor 方法将 ColorStateList 应用到 Button 的字体颜色上的,那接下来就进到 Button 的 SetTextColor 方法一看究竟。

//3. 进入 Button 的 setTextColor 方法  
public class TextView{
    
    ...
    
    @android.view.RemotableViewMethod
    public void setTextColor(ColorStateList colors) {
        if (colors == null) {
            throw new NullPointerException();
        }

        mTextColor = colors;
        updateTextColors();
    }
    
    ...
    
}

因为 Button 继承至 TextView,Button 的 SetTextColor 方法继承至 TextView,且未做任何更改,因此直接进入了 TextView 类中。

在 TextView 类的 SetTextColor 方法中调用了 UpdateTextColors 方法。

//4. 进入 updateTextColors 方法  
public class TextView{
    
    ...
    
    private void updateTextColors() {
        boolean inval = false;
        final int[] drawableState = getDrawableState();
        int color = mTextColor.getColorForState(drawableState, 0);
        if (color != mCurTextColor) {
            mCurTextColor = color;
            inval = true;
        }
        if (mLinkTextColor != null) {
            color = mLinkTextColor.getColorForState(drawableState, 0);
            if (color != mTextPaint.linkColor) {
                mTextPaint.linkColor = color;
                inval = true;
            }
        }
        if (mHintTextColor != null) {
            color = mHintTextColor.getColorForState(drawableState, 0);
            if (color != mCurHintTextColor) {
                mCurHintTextColor = color;
                if (mText.length() == 0) {
                    inval = true;
                }
            }
        }
        if (inval) {
            // Text needs to be redrawn with the new color
            if (mEditor != null) mEditor.invalidateTextDisplayList();
            invalidate();
        }
    }
    
    ...
    
}

接着看下在 TextView 类中,哪里都调用了 TextView 的 UpdateTextColors 方法。

最终找到了 TextView 的 DrawableStateChanged 方法,即在 TextView 的 DrawableStateChanged 方法中调用了 TextView 的 UpdateTextColors 方法。

//5. 进入 drawableStateChanged 方法  
public class TextView{
    
    ...
    
    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();

        if (mTextColor != null && mTextColor.isStateful()
                || (mHintTextColor != null && mHintTextColor.isStateful())
                || (mLinkTextColor != null && mLinkTextColor.isStateful())) {
            updateTextColors();
        }

        if (mDrawables != null) {
            final int[] state = getDrawableState();
            for (Drawable dr : mDrawables.mShowing) {
                if (dr != null && dr.isStateful() && dr.setState(state)) {
                    invalidateDrawable(dr);
                }
            }
        }
    }
    
    ...
    
}

在 TextView 类的 DrawableStateChanged 方法中调用了父类的 DrawableStateChanged 方法,进入 TextView 的父类(View)中看下哪里都调用了 DrawableStateChanged 方法。

最终找到了 View 的 RefreshDrawableState 方法,即在 View 的 RefreshDrawableState 方法中调用了 DrawableStateChanged 方法。

//6. 进入 refreshDrawableState 方法  
public class View{
    ...
    
    /**
     * Call this to force a view to update its drawable state. This will cause
     * drawableStateChanged to be called on this view. Views that are interested
     * in the new state should call getDrawableState.
     *
     * @see #drawableStateChanged
     * @see #getDrawableState
     */
    public void refreshDrawableState() {
        mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
        drawableStateChanged();

        ViewParent parent = mParent;
        if (parent != null) {
            parent.childDrawableStateChanged(this);
        }
    }
    
    ...
}

在 View 类中看下哪里都调用了 RefreshDrawableState 方法。

在 View 类中,发现有多个方法都调用了 RefreshDrawableState 方法,如:

  • setEnabled(boolean enabled)
  • setPressed(boolean pressed)
  • onWindowFocusChanged(boolean hasWindowFocus)
  • setHovered(boolean hovered)
  • setSelected(boolean selected)
  • setActivated(boolean activated)

是不是有一种似曾相识的感觉:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item
        android:color="hex_color"
        android:state_pressed=["true" | "false"]
        android:state_focused=["true" | "false"]
        android:state_selected=["true" | "false"]
        android:state_checkable=["true" | "false"]
        android:state_checked=["true" | "false"]
        android:state_enabled=["true" | "false"]
        android:state_window_focused=["true" | "false"] />
</selector>

接下来,我们随便挑一个方法来分析——SetPressed 方法。

//7. 进入 setPressed 方法  
public class View{
    ...
    
    /**
     * Sets the pressed state for this view.
     *
     * @see #isClickable()
     * @see #setClickable(boolean)
     *
     * @param pressed Pass true to set the View's internal state to "pressed", or false to reverts
     *        the View's internal state from a previously set "pressed" state.
     */
    public void setPressed(boolean pressed) {
        final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);

        if (pressed) {
            mPrivateFlags |= PFLAG_PRESSED;
        } else {
            mPrivateFlags &= ~PFLAG_PRESSED;
        }

        if (needsRefresh) {
            refreshDrawableState();
        }
        dispatchSetPressed(pressed);
    }
    
    ...
}

接下来看下,在 View 类中哪里都调用了 SetPressed 方法。

在 View 类中,发现有多个方法都调用了 SetPressed 方法,如:

  • removeUnsetPressCallback
  • onFocusChanged
  • resetPressedState
  • dispatchGenericMotionEventInternal
  • onKeyDown
  • onKeyUp
  • onTouchEvent

在上面的这些方法中,有一个方法引起了我们注意——onTouchEvent 处理触屏事件的方法。

Implement this method to handle touch screen motion events.

public class View{

    ...
    
    public boolean onTouchEvent(MotionEvent event) {
        
        ...

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                
                case MotionEvent.ACTION_UP:
                    ...
                    break;
                
                case MotionEvent.ACTION_DOWN:
                    
                    ...
                    
                    if (isInScrollingContainer) {
                        
                        ...
                        
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        ///
                        //                                                               //
                        //                       只看这里就好啦                          //
                        //                                                               //
                        ///
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;
                    
                case MotionEvent.ACTION_CANCEL:
                    ...
                    break;

                case MotionEvent.ACTION_MOVE:
                    ...
                    break;
                    
            }

            return true;
        }

        return false;
    }
    
    ...
    
}

到这里,我们不难发现最终 ColorStateList 是如何起作用的:

在 View 的 OnTouchEvent 中根据用户操作确定当前 View 的状态,选择与该状态对应的颜色值并将其设置到 View 的 Paint上,进而在刷新界面的时候应用新的颜色。在 TextView 系控件中表现为:根据 TextView 系控件的状态将与该状态对应的颜色值设置到当前控件的 TextPaint 上,进而在刷新界面的时候应用新的颜色。

4. ColorStateList 与 StateListDrawable 之间的关系

ColorStateList 与 StateListDrawable 其实并没有什么关系。

ColorStateList 继承至 Object,而 StateListDrawable 间接继承至 Drawable。

如果非要从它们两个中间找到共同点,那就是它们都能根据当前 View 的状态改变自己的显示内容(ColorStateList 根据 View 状态显示不同的 Color,StateListDrawable 根据 View 状态显示不同的 Drawable)。

5. 参考文献

  1. Color State List Resource
  2. ColorStateList
  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Sourcetree是一个非常好用的git客户端,它提供了可视化的界面帮助开发者进行多人协作开发过程中的各种git操作,比如push、pull、add、commit、merge等等。 在使用Sourcetree之前,你需要先下载、安装和配置好环境。然后你可以打开Sourcetree,它会展示一个初始界面,其中包含了本地仓库的相关信息。 Sourcetree是一个强大的工具,但本文只介绍了一些常用的功能,并没有记录一些使用频率较低的功能。工具的目的是帮助我们提高工作效率,所以我们只需要掌握最有用的部分即可。 在正文部分,你可以了解到Sourcetree的各种功能,以及对应的git命令和如何使用这些功能。它详细介绍了各种常用操作,帮助你更好地使用Sourcetree进行版本控制。[1,2] 使用Sourcetree可以很方便地进行分支的检出和关联远程分支。当你使用命令行检出分支后,你还需要执行一些操作来与远程分支进行关联。但是Sourcetree非常友好,当你点击克隆按钮时,它会自动帮助你与远程仓库进行关联。 总之,Sourcetree是一个功能强大且易于使用的git客户端,它为开发者提供了可视化的界面来管理和执行各种git操作,帮助我们更高效地进行协同开发。[1,2,3]<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Sourcetree 使用详解](https://blog.csdn.net/weixin_43837354/article/details/105936140)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [SourceTree使用教程图文详解](https://blog.csdn.net/qq_41153943/article/details/120814918)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值