草庐IT

Android Span进阶之路——ClickableSpan

精装机械师 2023-11-04 原文

一、前言

    在Android中,可以使用强大的标记(Span)对象来实现富文本展示,相比 HTML 而言更高效实用。关于 Android Span 的入门篇可以阅读 Android中强大的标记对象-Span。本文将对 ClickableSpan (可点击的Span)展开深入的学习。

二、基本使用

    查看Android Doc 文档可以知道,ClickableSpan 是一个抽象类,它有两个子类,分别是 URLSpanTextLinks.TextLinkSpan(从 API Level 28 开始支持),对于这两个类的使用,这里不做详细讲解,我们主要讲解下如何通过继承 ClickableSpan 实现可点击的标记。

2.1 ClicableSpan 源码剖析

    首先,我们先来看看 ClickableSpan 抽象类的源码:

public abstract class ClickableSpan extends CharacterStyle implements UpdateAppearance {
    private static int sIdCounter = 0;

    private int mId = sIdCounter++;

    /**
     * Performs the click action associated with this span.
     */
    public abstract void onClick(@NonNull View widget);

    /**
     * Makes the text underlined and in the link color.
     */
    @Override
    public void updateDrawState(@NonNull TextPaint ds) {
        ds.setColor(ds.linkColor);
        ds.setUnderlineText(true);
    }

    /**
     * Get the unique ID for this span.
     *
     * @return The unique ID.
     * @hide
     */
    public int getId() {
        return mId;
    }
}

    从上面的源码来看,ClickableSpan 抽象类非常简单,继承该类需要重写的方法也是比较少,其中抽象方法 onClick() 是必须实现,下面讲解重写方法所能实现的效果:

  • public abstract void onClick(@NonNull View widget):抽象方法,必须实现。用以相应可点击标记被点击时的事件相应处理。
  • public void updateDrawState(@NonNull TextPaint ds):配置绘制参数,可以用来更改绘制样式,比如文字颜色、背景颜色、链接颜色、是否包含下划线等等。如果不重载此方法,将会使用默认的绘制样式。

2.2 自定义 ClickableSpan

    从前面的源码我们了解到 ClickableSpan 的成员方法,实现自己的自定义 ClickableSpan 就非常容易了:

/**
 * 自定义 ClickableSpan
 * @param textColor 可点击标记文字颜色
 * @param clickListener 点击时间监听
 */
class CSClickableSpan (@param:ColorInt private val textColor: Int,
                       private val clickListener: View.OnClickListener?) : ClickableSpan() {
    override fun onClick(widget: View) {
        clickListener?.onClick(widget)
    }

    override fun updateDrawState(ds: TextPaint) {
        super.updateDrawState(ds)

        ds.color = textColor // 字体颜色(前景色)
        ds.bgColor = Color.TRANSPARENT  // 背景颜色
        ds.linkColor = textColor // 链接颜色
        ds.isUnderlineText = false // 是否显示下划线
        // 这里还可以配置其他绘制样式,比如下划线的粗细(如果启用下划线)、字体等等
    }
}

2.3 使用自定义的 ClickableSpan

    接下来就可以在 SpannableString 或者 SpannableStringBuilder 中使用自定义的 CSClickableSpan 类。

val tvNormal = findViewById<TextView>(R.id.tv_normal_clickable_span)
// 必须设置 TextView 的 movementMethod 为 LinkMovementMethod,否则标记无法响应点击事件
tvNormal.movementMethod = LinkMovementMethod.getInstance()
tvNormal.setText(SpannableString("我是普通的ClickableSpan").apply {
    setSpan(CSClickableSpan(Color.BLUE, View.OnClickListener {
        Toast.makeText(this@ClickableSpanActivity, tvNormal.text, Toast.LENGTH_SHORT).show()
    }), 5, 18, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
})

注意事项:在 TextView 中使用 ClickableSpan 时,必须要设置 TextView 对象的 movementMethod 属性为 LinkMovementMethod,否则 ClickableSpan 标记不会响应点击事件。

    运行之后,可以看看效果。

  • 点击前
  • 点击后

        根据上面的例子,我们会发现标记点击后,会有一个背景色,其实这个背景色是 TextView 的高亮颜色,因为 LinkMovementMethod 在标记点击后,会选中标记部分文本。解决这个问题也很简单,只要将 TextViewhighlightColor 设置为透明即可,如下示例:
val tvNormalNoSelection = findViewById<TextView>(R.id.tv_normal_clickable_span_no_selection)
// 将 TextView 的高两色设置为透明,可去除点击后的选择高亮色
tvNormalNoSelection.highlightColor = Color.TRANSPARENT
// 必须设置 TextView 的 movementMethod 为 LinkMovementMethod,否则标记无法响应点击事件
tvNormalNoSelection.movementMethod = LinkMovementMethod.getInstance()
tvNormalNoSelection.setText(SpannableString("我是普通的ClickableSpan(无选中背景)").apply {
    setSpan(CSClickableSpan(Color.BLUE, View.OnClickListener {
        Toast.makeText(this@ClickableSpanActivity, tvNormalNoSelection.text, Toast.LENGTH_SHORT).show()
    }), 5, 18, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
})

    运行之后看效果,点击标记之后选中高亮色为透明,看起来就是没有高两色的效果,如下图:

三、高手进阶

3.1 在 ClickableSpan 中实现点击效果

    在前面篇幅中,虽然可以去掉标记选中高亮色,但是这样也并不完美,没有点击效果,用户体验还是有所欠缺。我们首先会想到用TextView 的高亮色,然而高亮色只能设置整型的颜色值,并不能设置ColorList。于是就猜想通过 TextView 的高亮色结合自定义的 CSClickableSpan 实现,笔者刚开始也是从这个角度着手,预想将高亮色设置成按下状态颜色,然后再将高亮色设置为透明色,后来发现这样无法实现,因为 ClickableSpan 这个过程中,会在 onClick() 方法调用之前,前后均会调用 updateDrawState() 更新绘制文本,在如此的调用逻辑下,这种方案是不可行的。既然无法从 TextView 下手,在示例代码中,我们唯一能寄予希望的就是 TextViewmovementMethod 属性了(也就是 LinkMovementMethod)。

  • LinkMovementMethod类源码剖析
package android.text.method;

import android.annotation.UnsupportedAppUsage;
import android.os.Build;
import android.text.Layout;
import android.text.NoCopySpan;
import android.text.Selection;
import android.text.Spannable;
import android.text.style.ClickableSpan;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.textclassifier.TextLinks.TextLinkSpan;
import android.widget.TextView;

/**
 * A movement method that traverses links in the text buffer and scrolls if necessary.
 * Supports clicking on links with DPad Center or Enter.
 */
public class LinkMovementMethod extends ScrollingMovementMethod {
    private static final int CLICK = 1;
    private static final int UP = 2;
    private static final int DOWN = 3;

    private static final int HIDE_FLOATING_TOOLBAR_DELAY_MS = 200;

    @Override
    public boolean canSelectArbitrarily() {
        return true;
    }

    @Override
    protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
            int movementMetaState, KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_CENTER:
            case KeyEvent.KEYCODE_ENTER:
                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
                    if (event.getAction() == KeyEvent.ACTION_DOWN &&
                            event.getRepeatCount() == 0 && action(CLICK, widget, buffer)) {
                        return true;
                    }
                }
                break;
        }
        return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
    }

    @Override
    protected boolean up(TextView widget, Spannable buffer) {
        if (action(UP, widget, buffer)) {
            return true;
        }

        return super.up(widget, buffer);
    }

    @Override
    protected boolean down(TextView widget, Spannable buffer) {
        if (action(DOWN, widget, buffer)) {
            return true;
        }

        return super.down(widget, buffer);
    }

    @Override
    protected boolean left(TextView widget, Spannable buffer) {
        if (action(UP, widget, buffer)) {
            return true;
        }

        return super.left(widget, buffer);
    }

    @Override
    protected boolean right(TextView widget, Spannable buffer) {
        if (action(DOWN, widget, buffer)) {
            return true;
        }

        return super.right(widget, buffer);
    }

    private boolean action(int what, TextView widget, Spannable buffer) {
        Layout layout = widget.getLayout();

        int padding = widget.getTotalPaddingTop() +
                      widget.getTotalPaddingBottom();
        int areaTop = widget.getScrollY();
        int areaBot = areaTop + widget.getHeight() - padding;

        int lineTop = layout.getLineForVertical(areaTop);
        int lineBot = layout.getLineForVertical(areaBot);

        int first = layout.getLineStart(lineTop);
        int last = layout.getLineEnd(lineBot);

        ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class);

        int a = Selection.getSelectionStart(buffer);
        int b = Selection.getSelectionEnd(buffer);

        int selStart = Math.min(a, b);
        int selEnd = Math.max(a, b);

        if (selStart < 0) {
            if (buffer.getSpanStart(FROM_BELOW) >= 0) {
                selStart = selEnd = buffer.length();
            }
        }

        if (selStart > last)
            selStart = selEnd = Integer.MAX_VALUE;
        if (selEnd < first)
            selStart = selEnd = -1;

        switch (what) {
            case CLICK:
                if (selStart == selEnd) {
                    return false;
                }

                ClickableSpan[] links = buffer.getSpans(selStart, selEnd, ClickableSpan.class);

                if (links.length != 1) {
                    return false;
                }

                ClickableSpan link = links[0];
                if (link instanceof TextLinkSpan) {
                    ((TextLinkSpan) link).onClick(widget, TextLinkSpan.INVOCATION_METHOD_KEYBOARD);
                } else {
                    link.onClick(widget);
                }
                break;

            case UP:
                int bestStart, bestEnd;

                bestStart = -1;
                bestEnd = -1;

                for (int i = 0; i < candidates.length; i++) {
                    int end = buffer.getSpanEnd(candidates[i]);

                    if (end < selEnd || selStart == selEnd) {
                        if (end > bestEnd) {
                            bestStart = buffer.getSpanStart(candidates[i]);
                            bestEnd = end;
                        }
                    }
                }

                if (bestStart >= 0) {
                    Selection.setSelection(buffer, bestEnd, bestStart);
                    return true;
                }

                break;

            case DOWN:
                bestStart = Integer.MAX_VALUE;
                bestEnd = Integer.MAX_VALUE;

                for (int i = 0; i < candidates.length; i++) {
                    int start = buffer.getSpanStart(candidates[i]);

                    if (start > selStart || selStart == selEnd) {
                        if (start < bestStart) {
                            bestStart = start;
                            bestEnd = buffer.getSpanEnd(candidates[i]);
                        }
                    }
                }

                if (bestEnd < Integer.MAX_VALUE) {
                    Selection.setSelection(buffer, bestStart, bestEnd);
                    return true;
                }

                break;
        }

        return false;
    }

    @Override
    public boolean onTouchEvent(TextView widget, Spannable buffer,
                                MotionEvent event) {
        int action = event.getAction();

        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
            int x = (int) event.getX();
            int y = (int) event.getY();

            x -= widget.getTotalPaddingLeft();
            y -= widget.getTotalPaddingTop();

            x += widget.getScrollX();
            y += widget.getScrollY();

            Layout layout = widget.getLayout();
            int line = layout.getLineForVertical(y);
            int off = layout.getOffsetForHorizontal(line, x);

            ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);

            if (links.length != 0) {
                ClickableSpan link = links[0];
                if (action == MotionEvent.ACTION_UP) {
                    if (link instanceof TextLinkSpan) {
                        ((TextLinkSpan) link).onClick(
                                widget, TextLinkSpan.INVOCATION_METHOD_TOUCH);
                    } else {
                        link.onClick(widget);
                    }
                } else if (action == MotionEvent.ACTION_DOWN) {
                    if (widget.getContext().getApplicationInfo().targetSdkVersion
                            >= Build.VERSION_CODES.P) {
                        // Selection change will reposition the toolbar. Hide it for a few ms for a
                        // smoother transition.
                        widget.hideFloatingToolbar(HIDE_FLOATING_TOOLBAR_DELAY_MS);
                    }
                    Selection.setSelection(buffer,
                            buffer.getSpanStart(link),
                            buffer.getSpanEnd(link));
                }
                return true;
            } else {
                Selection.removeSelection(buffer);
            }
        }

        return super.onTouchEvent(widget, buffer, event);
    }

    @Override
    public void initialize(TextView widget, Spannable text) {
        Selection.removeSelection(text);
        text.removeSpan(FROM_BELOW);
    }

    @Override
    public void onTakeFocus(TextView view, Spannable text, int dir) {
        Selection.removeSelection(text);

        if ((dir & View.FOCUS_BACKWARD) != 0) {
            text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
        } else {
            text.removeSpan(FROM_BELOW);
        }
    }

    public static MovementMethod getInstance() {
        if (sInstance == null)
            sInstance = new LinkMovementMethod();

        return sInstance;
    }

    @UnsupportedAppUsage
    private static LinkMovementMethod sInstance;
    private static Object FROM_BELOW = new NoCopySpan.Concrete();
}

    源码有点多,但是我们的目标是实现点击效果,那么肯定跟触摸事件相关,所以,我们需要处理的也就是 onTouchEvent() 方法。接下来,我们通过继承 LinkMovementMethod 来自定义一个MovementMethod 类,在onTouchEvent() 方法中的 MotionEvent.ACTION_DOWNMotionEvent.ACTION_UP 事件中添加处理逻辑。

/**
 * 可点击标记 MovementMethod
 * @param clickedBgColor 按下背景颜色
 */
class ClickableSpanMovementMethod(@ColorInt val clickedBgColor: Int) : LinkMovementMethod() {
    override fun onTouchEvent(widget: TextView?, buffer: Spannable?,  event: MotionEvent?): Boolean {
        if(null == event || null == widget || null == buffer) {
            return false
        }
        val action = event.action
        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
            var x = event.x.toInt()
            var y = event.y.toInt()
            x -= widget.totalPaddingLeft
            y -= widget.totalPaddingTop
            x += widget.scrollX
            y += widget.scrollY
            val layout = widget.layout
            val line = layout.getLineForVertical(y)
            val off = layout.getOffsetForHorizontal(line, x.toFloat())
            val links = buffer!!.getSpans(off, off, ClickableSpan::class.java)
            if (links.isNotEmpty()) {
                val link = links[0]
                if (action == MotionEvent.ACTION_UP) {
                    // ACTION_UP 移除选中
                    Selection.removeSelection(buffer)
                    link.onClick(widget)
                } else if (action == MotionEvent.ACTION_DOWN) {
                    // ACTION_DOWN 设置高亮色为点击色,并选中标记
                    widget.highlightColor = clickedBgColor
                    Selection.setSelection(
                        buffer,
                        buffer.getSpanStart(link),
                        buffer.getSpanEnd(link)
                    )
                }
                return true
            } else {
                Selection.removeSelection(buffer)
            }
        }
        return super.onTouchEvent(widget, buffer, event)
    }
}

    然后将 TextViewmovementMethod 属性值设置为自定义的 MovementMethod 实例对象即可:

val tvStyle = findViewById<TextView>(R.id.tv_clickstyle_clickable_span)
tvStyle.movementMethod = ClickableSpanMovementMethod(Color.argb(0x20, 0x33, 0x33, 0x33))
tvStyle.setText(SpannableString("我是带点击效果的ClickableSpan").apply {
    setSpan(CSClickableSpan(Color.BLUE, View.OnClickListener {
        Toast.makeText(this@ClickableSpanActivity, tvStyle.text, Toast.LENGTH_SHORT).show()
    }), 8, 21, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
})
  • 实现效果

        至此,已经完美实现 ClickableSpan 点击效果。上面的示例是通过改变选中高亮色来实现的,下面是通过给 ClickableSpan 重叠一个 BackgroundColorSpan 的实现方案,效果完全一致,代码如下所示:
/**
 * 可点击标记 MovementMethod
 * @param clickedBgColor 按下背景颜色
 */
class ClickableSpanMovementMethod(@ColorInt val clickedBgColor: Int) : LinkMovementMethod() {
    override fun onTouchEvent(widget: TextView?, buffer: Spannable?, event: MotionEvent?): Boolean {
        if(null == event || null == widget || null == buffer) {
            return false
        }
        val action = event.action
        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
            var x = event.x.toInt()
            var y = event.y.toInt()
            x -= widget.totalPaddingLeft
            y -= widget.totalPaddingTop
            x += widget.scrollX
            y += widget.scrollY
            val layout = widget.layout
            val line = layout.getLineForVertical(y)
            val off = layout.getOffsetForHorizontal(line, x.toFloat())
            val links = buffer!!.getSpans(off, off, ClickableSpan::class.java)
            if (links.isNotEmpty()) {
                val link = links[0]
                if (action == MotionEvent.ACTION_UP) {
                    // ACTION_UP 给当前标记添加一个透明色的背景Span
                    buffer.setSpan(
                        BackgroundColorSpan(Color.TRANSPARENT), buffer.getSpanStart(link),
                        buffer.getSpanEnd(link), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
                    // 移除选中(如果将TextView高亮色设置为透明,可忽略此行代码)
                    Selection.removeSelection(buffer)
                    link.onClick(widget)
                } else if (action == MotionEvent.ACTION_DOWN) {
                    // ACTION_DOWN 给当前标记添加一个点击色的背景Span
                    buffer.setSpan(BackgroundColorSpan(clickedBgColor), buffer.getSpanStart(link),
                        buffer.getSpanEnd(link), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
                    // 移除选中(如果将TextView高亮色设置为透明,可忽略此行代码)
                    Selection.removeSelection(buffer)
                }
                return true
            } else {
                Selection.removeSelection(buffer)
            }
        }
//            return false
        return super.onTouchEvent(widget, buffer, event)
    }
}

3.2 在ClickableSpan中实现点击改变标记字体颜色

    我们知道,在实例化 Span 对象时,我们只能传 int 类型的颜色值,无法传入 ColorList 类型,因此点击时改变标记字体颜色,也必须通过自定义才能实现效果。有了前车之鉴,实现点击时改变字体颜色就很容易了。在上一章节中提到的实现点击效果,可以通过给文字一个叠加的背景色标记来实现,那么改变文字颜色,就可以通过叠加一个前景色标记来实现。下面直接上代码:

/**
 * 可点击标记 MovementMethod
 * @param clickedBgColor 按下背景颜色
 * @param normalTextColor 普通模式下文字颜色
 * @param clickedTextColor 按下文字颜色
 */
class ClickableSpanMovementMethod(@ColorInt val clickedBgColor: Int, @ColorInt val normalTextColor : Int,
                                  @ColorInt val clickedTextColor: Int) : LinkMovementMethod() {
    override fun onTouchEvent(widget: TextView?, buffer: Spannable?, event: MotionEvent?): Boolean {
        if(null == event || null == widget || null == buffer) {
            return false
        }
        val action = event.action
        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
            var x = event.x.toInt()
            var y = event.y.toInt()
            x -= widget.totalPaddingLeft
            y -= widget.totalPaddingTop
            x += widget.scrollX
            y += widget.scrollY
            val layout = widget.layout
            val line = layout.getLineForVertical(y)
            val off = layout.getOffsetForHorizontal(line, x.toFloat())
            val links = buffer!!.getSpans(off, off, ClickableSpan::class.java)
            if (links.isNotEmpty()) {
                val link = links[0]
                if (action == MotionEvent.ACTION_UP) {
                    // ACTION_UP 给当前标记添加一个透明色的背景Span
                    buffer.setSpan(
                        BackgroundColorSpan(Color.TRANSPARENT), buffer.getSpanStart(link),
                        buffer.getSpanEnd(link), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
                    // ACTION_UP 恢复普通字体颜色
                    buffer.setSpan(ForegroundColorSpan(normalTextColor), buffer.getSpanStart(link),
                        buffer.getSpanEnd(link), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

                    // ACTION_UP 移除选中
                    // 移除选中(如果将TextView高亮色设置为透明,可忽略此行代码)
                    Selection.removeSelection(buffer)
                    link.onClick(widget)
                } else if (action == MotionEvent.ACTION_DOWN) {
                    // ACTION_DOWN 给当前标记添加一个点击色的背景Span
                    buffer.setSpan(BackgroundColorSpan(clickedBgColor), buffer.getSpanStart(link),
                        buffer.getSpanEnd(link), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
                    // ACTION_DOWN 给当前标记添加一个点击色的前景Span
                    buffer.setSpan(ForegroundColorSpan(clickedTextColor), buffer.getSpanStart(link),
                        buffer.getSpanEnd(link), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
                    // 移除选中(如果将TextView高亮色设置为透明,可忽略此行代码)
                    Selection.removeSelection(buffer)
                }
                return true
            } else {
                Selection.removeSelection(buffer)
            }
        }
        return super.onTouchEvent(widget, buffer, event)
    }
}
  • 实现效果

有关Android Span进阶之路——ClickableSpan的更多相关文章

  1. 焕新古文化传承之路,AI为古彝文识别赋能 - 2

    目录1古彝文与古典保护2古文识别的挑战2.1西文与汉文OCR2.2古彝文识别难点3合合信息:古彝文保护新思路3.1图像矫正3.2图像增强3.3语义理解3.4工程技巧4总结1古彝文与古典保护彝文指的是云南、贵州、四川等地的彝族人使用的文字,区别于现代意义上的彝文,古彝文指的是在民间流通使用的原生态彝文,多达87046字。古彝文的起源距今至少数千年,是世界上最古老的文字之一。对古彝文字集研究有助于理解尚未被翻译成汉文、用字尚未规范化的古籍,更深层、透彻地作用于传统文化保护。古彝文字义对照图(网络资料+邵文苑供图)古籍是不可再生的宝贵资源,应当得到妥善保护。中国的古籍在历史上迭经水火兵燹等自然灾害、

  2. 【思考】聊聊低代码的实践之路 - 2

    文章目录背景一、最初的疑惑二、简单聊聊原理三、组织内实践案例四、实践带来的反思五、最后聊几句问题背景这个概念由来已久,但是在国内兴起,是最近几年;低代码即Low-Code;指提供可视化开发环境,可以用来创建和管理软件应用;简单的说就是可以通过各种组件的拖拽,实现页面的创建,交互流程和逻辑,以及数据层面的管理,更加高效的实现需求;早先在数据公司时;见识过低代码的应用,也参与过部分研发,比如元数据平台,BI分析等;不过,当时还是以数据管理的工具来定义项目,并非是低代码;从「2020年底」开始;实际上,那个时间节点,低代码平台的应用已经形成趋势了;现在的公司,将低代码平台的使用规划到业务体系中;后来

  3. 「前端代码简洁之路」后台系统之详情页设计 - 2

    一、乱花迷人眼我就是被迷的那双眼。有时候需求来了,用熟悉的套路进行开发,确实很节省时间也能保证功能的稳定,但是这些开发的惯性无形中阻碍了我对技术的探索。我一直想改造详情页,解放重复功能开发的劳动力,但是详情页一眼望都是内容平铺,好像并没有什么可做的代码设计。后来我拨开繁花,发现详情页的组件化不必想的过于复杂,后台系统风格统一即可。因为大部分的详情页面是内容的展示,偶尔会出现少量的操作功能。将风格统一的部分进行组件化处理,操作功能使用回调函数放回当前页面,避免组件里做过多的业务逻辑。看,这不就成了。项目基于React框架开发的,所以代码写法是JSX语法,组件开发使用的hooks函数式组件,UI框

  4. 大家沉迷短视频无法自拔?Python爬虫进阶,带你玩转短视频 - 2

    大家好,我是辣条。现在短视频可谓是一骑绝尘,吃饭的时候、休息的时候、躺在床上都在刷短视频,今天给大家带来python爬虫进阶:美拍视频地址加密解析。短视频js逆向解析抓取目标工具使用重点学习内容项目思路解析抓取目标目标网址:美拍视频工具使用开发环境:win10、python3.7开发工具:pycharm、Chrome工具包:requests、xpath、base64重点学习内容爬虫采集数据的解析过程js代码调试技巧js逆向解析代码Python代码的转换项目思路解析进入到网站的首页挑选你感兴趣的分类根据首页地址获取到进入详情页面的超链接的跳转地址找到对应加密的视频播放地址数据这个数据是静态的网页

  5. 【JavaEE进阶】——第二节.Spring核心和设计思想 - 2

    文章目录前言一、Spring是什么?二、什么是容器?三、什么是IoC?3.1初始loC3.2举例解释loC3.3 SpringIoC思想的体现四、什么是DI?4.1DI的概念4.2 Ioc和DI的区别总结前言今天我们将进入到有关spring的认识当中,要使用它的前提就是要认识并熟悉它,上一节我们介绍了有关maven的配置,必须要配置完成后,才能完成我们后面的学习工作,让我们进入到今天的学习当中吧!!!!!!!!!一、Spring是什么?概念:我们通常所说的Spring指的是SpringFramework(Spring框架),它是⼀个开源框架,有着活跃⽽庞⼤的社区,这就是它之所以能⻓久不衰的原因

  6. 【C语言进阶】还说不会?一文带你全面掌握计算机预处理操作 - 2

    目录🍊前言🍊:🍈一、宏与函数🍈:        1.宏与函数对比:    2.宏与函数的命名约定:🍓二、预处理操作符🍓:    1.预处理操作符"#":    2.预处理操作符"##":🥝三、条件编译🥝:    1.简述条件编译指令:    2.常见条件编译指令:🍒总结🍒:🛰️博客主页:✈️銮同学的干货分享基地🛰️欢迎关注:👍点赞🙌收藏✍️留言🛰️系列专栏:💐【进阶】C语言学习            🧧  C语言学习🛰️代码仓库:🎉VS2022_C语言仓库    家人们更新不易,你们的👍点赞👍和⭐关注⭐真的对我真重要,各位路过的友友麻烦多多点赞关注,欢迎你们的私信提问,感谢你们的转发!    

  7. 奇舞周刊第486期:ChatGPT 的狂飙之路 - 2

    记得点击文章末尾的“ 阅读原文 ”查看哟~下面先一起看下本期周刊 摘要 吧~奇舞推荐■■■ ChatGPT的狂飙之路最近随着ChatGPT爆火出圈,网络上各种关于ChatGPT的争论声也不断;有些人把它当成一个更高级的聊天机器人,有人兴奋地看到了创业的风口,而另一些人对它取代人类的工作露出了不少担忧;那么它到底是推动社会不断前进的工具,还是妄图颠覆人类社会的T-1000?本文我们来深入的探讨一下ChatGPT的那些事。 带你看看前端生态圈的技术趋势今年的state-of-css调查共回收了14310份问卷结果,state-of-js调查共回收了39472份问卷结果,希望各位能在这些数据和分析中

  8. 云计算学习之路——LVS负载均衡 - 2

    LVS文章目录LVS一、负载均衡集群介绍1、集群是什么?2、负载均衡集群技术3、负载均衡集群技术实现方式和产品4、负载均衡实现效果图5、负载均衡分类6、四层负载均衡与七层负载均衡的区别二、LVS介绍三、LVS工作模式1、LVS负载均衡的四种工作模式2、四种工作模式的原理、优缺点3、四种工作模式的区别四、LVS管理工具——ipvsadm五、LVS负载均衡集群实战应用1、环境:2、搭建web服务器3、LVS负载均衡配置4、验证六、LVS的调度算法1、静态算法2、动态算法七、LVS健康监测脚本一、负载均衡集群介绍1、集群是什么?集群技术是一种较新的技术,可以在付出较低成本的情况下获得在性能、可靠性、

  9. 上知天文,下知地理,还能替人写脚本!人工智能的进阶ChatGPT - 2

    ChatGPT是OpenAI在11月30日推出的聊天机器人,于12月1日起对公众免费开放。自从这东西出来之后,大家对此的讨论热情越发浓烈。ChatGPT具体可以干些什么?帮你写论文、检讨书、情书,甚至情诗也能信手拈来。以上都是网友测试它写出来的内容,但仔细一看,这些虽然有框架在,但基本上都是车轱辘话来回倒腾。如果真的说用来取代人类,还为时过早,而这些AI技术的本意也是为了提高生产率。除了写文案的能力让大家震惊,其中最震惊的还是它的编程能力。是的,它可以帮你写代码。它还可以帮你debug,直接指出你这段代码的问题和优化方式。没有深入尝试,只是确认了下有这个功能。刷算法题啥的,更是不在话下。随便在

  10. 【Python百日进阶-Web开发-Feffery】Day390 - fac反馈05:AntdNotification通知提醒框 - 2

    文章目录前言:fac是什么?“人生苦短,我用Python;Web开发,首选Feffery!”↓↓↓今日笔记↓↓↓五、fac反馈:AntdNotification通知提醒框5.1语法与参数5.1.1语法5.1.2主要参数说明5.2使用示例5.2.1基础使用5.2.2不同的状态5.2.3不同的弹出位置5.2.4持续显示时长的设置前言:fac是什么?feffery-antd-components(简称fac),是国内大佬费弗里(Feffery)老师基于著名的Rea

随机推荐