Android-TextView跑馬燈探秘

前言

自定義View實(shí)現(xiàn)的跑馬燈一直沒(méi)有實(shí)現(xiàn)類(lèi)似 Android TextView 的跑馬燈首尾相接的效果,所以一直想看看Android TextView 的跑馬燈是如何實(shí)現(xiàn)

本文主要探秘 Android TextView 的跑馬燈實(shí)現(xiàn)原理及實(shí)現(xiàn)自下往上效果的跑馬燈

探秘

TextView#onDraw

原生 Android TextView 如何設(shè)置開(kāi)啟跑馬燈效果,此處不再描述

View 的繪制都在 onDraw 方法中,這里直接查看 TextView#onDraw() 方法,刪減一些不關(guān)心的代碼

protected void onDraw(Canvas canvas) {
    // 是否需要重新啟動(dòng)跑馬燈
    restartMarqueeIfNeeded();

    // Draw the background for this view
    super.onDraw(canvas);
        
    // 刪減不關(guān)心的代碼

    // 創(chuàng)建`mLayout`對(duì)象, 此處為`StaticLayout`
    if (mLayout == null) {
        assumeLayout();
    }

    Layout layout = mLayout;

    canvas.save();

    // 刪減不關(guān)心的代碼

    final int layoutDirection = getLayoutDirection();
    final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);

    // 判斷跑馬燈設(shè)置項(xiàng)是否正確
    if (isMarqueeFadeEnabled()) {
        if (!mSingleLine && getLineCount() == 1 && canMarquee()
              && (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
           final int width = mRight - mLeft;
           final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
           final float dx = mLayout.getLineRight(0) - (width - padding);
           canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
        }

        // 判斷跑馬燈是否啟動(dòng)
        if (mMarquee != null && mMarquee.isRunning()) {
            final float dx = -mMarquee.getScroll();
            // 移動(dòng)畫(huà)布
            canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
        }
    }

    final int cursorOffsetVertical = voffsetCursor - voffsetText;

    Path highlight = getUpdatedHighlightPath();
    if (mEditor != null) {
        mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
    } else {
        // 繪制文本
        layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
    }

    // 判斷是否可以繪制尾部文本
    if (mMarquee != null && mMarquee.shouldDrawGhost()) {
        final float dx = mMarquee.getGhostOffset();
        // 移動(dòng)畫(huà)布
        canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
        // 繪制尾部文本
        layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
    }

    canvas.restore();
}

Marquee

根據(jù) onDraw() 方法分析,跑馬燈效果的實(shí)現(xiàn)主要依賴(lài) mMarquee 這個(gè)對(duì)象來(lái)實(shí)現(xiàn),好的,看下 Marquee 吧,Marquee 代碼較少,就貼上全部源碼吧

private static final class Marquee {
    // TODO: Add an option to configure this
    // 縮放相關(guān),不關(guān)心此字段
    private static final float MARQUEE_DELTA_MAX = 0.07f;
    
    // 跑馬燈跑完一次后多久開(kāi)始下一次
    private static final int MARQUEE_DELAY = 1200;
    
    // 繪制一次跑多長(zhǎng)距離因子,此字段與速度相關(guān)
    private static final int MARQUEE_DP_PER_SECOND = 30;

    // 跑馬燈狀態(tài)常量
    private static final byte MARQUEE_STOPPED = 0x0;
    private static final byte MARQUEE_STARTING = 0x1;
    private static final byte MARQUEE_RUNNING = 0x2;

    // 對(duì)TextView進(jìn)行弱引用
    private final WeakReference<TextView> mView;
    
    // 幀率相關(guān)
    private final Choreographer mChoreographer;

    // 狀態(tài)
    private byte mStatus = MARQUEE_STOPPED;
    
    // 繪制一次跑多長(zhǎng)距離
    private final float mPixelsPerMs;
    
    // 最大滾動(dòng)距離
    private float mMaxScroll;
    
    // 是否可以繪制右陰影, 右側(cè)淡入淡出效果
    private float mMaxFadeScroll;
    
    // 尾部文本什么時(shí)候開(kāi)始繪制
    private float mGhostStart;
    
    // 尾部文本繪制位置偏移量
    private float mGhostOffset;
    
    // 是否可以繪制左陰影,左側(cè)淡入淡出效果
    private float mFadeStop;
    
    // 重復(fù)限制
    private int mRepeatLimit;

    // 跑動(dòng)距離
    private float mScroll;
    
    // 最后一次跑動(dòng)時(shí)間,單位毫秒
    private long mLastAnimationMs;

    Marquee(TextView v) {
        final float density = v.getContext().getResources().getDisplayMetrics().density;
        // 計(jì)算每次跑多長(zhǎng)距離
        mPixelsPerMs = MARQUEE_DP_PER_SECOND * density / 1000f;
        mView = new WeakReference<TextView>(v);
        mChoreographer = Choreographer.getInstance();
    }

    // 幀率回調(diào),用于跑馬燈跑動(dòng)
    private Choreographer.FrameCallback mTickCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            tick();
        }
    };

    // 幀率回調(diào),用于跑馬燈開(kāi)始跑動(dòng)
    private Choreographer.FrameCallback mStartCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            mStatus = MARQUEE_RUNNING;
            mLastAnimationMs = mChoreographer.getFrameTime();
            tick();
        }
    };

    // 幀率回調(diào),用于跑馬燈重新跑動(dòng)
    private Choreographer.FrameCallback mRestartCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            if (mStatus == MARQUEE_RUNNING) {
                if (mRepeatLimit >= 0) {
                    mRepeatLimit--;
                }
                start(mRepeatLimit);
            }
        }
    };

    // 跑馬燈跑動(dòng)實(shí)現(xiàn)
    void tick() {
        if (mStatus != MARQUEE_RUNNING) {
            return;
        }

        mChoreographer.removeFrameCallback(mTickCallback);

        final TextView textView = mView.get();
        // 判斷TextView是否處于獲取焦點(diǎn)或選中狀態(tài)
        if (textView != null && (textView.isFocused() || textView.isSelected())) {
            // 獲取當(dāng)前時(shí)間
            long currentMs = mChoreographer.getFrameTime();
            // 計(jì)算當(dāng)前時(shí)間與上次時(shí)間的差值
            long deltaMs = currentMs - mLastAnimationMs;
            mLastAnimationMs = currentMs;
            // 根據(jù)時(shí)間差計(jì)算本次跑動(dòng)的距離,減輕視覺(jué)上跳動(dòng)/卡頓
            float deltaPx = deltaMs * mPixelsPerMs;
            // 計(jì)算跑動(dòng)距離
            mScroll += deltaPx;
            // 判斷是否已經(jīng)跑完
            if (mScroll > mMaxScroll) {
                mScroll = mMaxScroll;
                // 發(fā)送重新開(kāi)始跑動(dòng)事件
                mChoreographer.postFrameCallbackDelayed(mRestartCallback, MARQUEE_DELAY);
            } else {
                // 發(fā)送下一次跑動(dòng)事件
                mChoreographer.postFrameCallback(mTickCallback);
            }
            // 調(diào)用此方法會(huì)觸發(fā)執(zhí)行`onDraw`方法
            textView.invalidate();
        }
    }

    // 停止跑馬燈
    void stop() {
        mStatus = MARQUEE_STOPPED;
        mChoreographer.removeFrameCallback(mStartCallback);
        mChoreographer.removeFrameCallback(mRestartCallback);
        mChoreographer.removeFrameCallback(mTickCallback);
        resetScroll();
    }

    private void resetScroll() {
        mScroll = 0.0f;
        final TextView textView = mView.get();
        if (textView != null) textView.invalidate();
    }

    // 啟動(dòng)跑馬燈
    void start(int repeatLimit) {
        if (repeatLimit == 0) {
            stop();
            return;
        }
        mRepeatLimit = repeatLimit;
        final TextView textView = mView.get();
        if (textView != null && textView.mLayout != null) {
            // 設(shè)置狀態(tài)為在跑
            mStatus = MARQUEE_STARTING;
            // 重置跑動(dòng)距離
            mScroll = 0.0f;
            // 計(jì)算TextView寬度
            final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft()
                - textView.getCompoundPaddingRight();
            // 獲取文本第0行的寬度
            final float lineWidth = textView.mLayout.getLineWidth(0);
            // 取TextView寬度的三分之一
            final float gap = textWidth / 3.0f;
            // 計(jì)算什么時(shí)候可以開(kāi)始繪制尾部文本:首部文本跑動(dòng)到哪里可以繪制尾部文本
            mGhostStart = lineWidth - textWidth + gap;
            // 計(jì)算最大滾動(dòng)距離:什么時(shí)候認(rèn)為跑完一次
            mMaxScroll = mGhostStart + textWidth;
            // 尾部文本繪制偏移量
            mGhostOffset = lineWidth + gap;
            // 跑動(dòng)到哪里時(shí)不繪制左側(cè)陰影
            mFadeStop = lineWidth + textWidth / 6.0f;
            // 跑動(dòng)到哪里時(shí)不繪制右側(cè)陰影
            mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;

            textView.invalidate();
            // 開(kāi)始跑動(dòng)
            mChoreographer.postFrameCallback(mStartCallback);
        }
    }

    // 獲取尾部文本繪制位置偏移量
    float getGhostOffset() {
        return mGhostOffset;
    }

    // 獲取當(dāng)前滾動(dòng)距離
    float getScroll() {
        return mScroll;
    }

    // 獲取可以右側(cè)陰影繪制的最大距離
    float getMaxFadeScroll() {
        return mMaxFadeScroll;
    }

    // 判斷是否可以繪制左側(cè)陰影
    boolean shouldDrawLeftFade() {
        return mScroll <= mFadeStop;
    }

    // 判斷是否可以繪制尾部文本
    boolean shouldDrawGhost() {
        return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
    }

    // 跑馬燈是否在跑
    boolean isRunning() {
        return mStatus == MARQUEE_RUNNING;
    }

    // 跑馬燈是否不跑
    boolean isStopped() {
        return mStatus == MARQUEE_STOPPED;
    }
}

好的,分析完 Marquee,跑馬燈實(shí)現(xiàn)原理豁然明亮

  1. TextView 開(kāi)啟跑馬燈效果時(shí)調(diào)用 Marquee#start() 方法
  2. Marquee#start() 方法中觸發(fā) TextView 重繪,開(kāi)始計(jì)算跑動(dòng)距離
  3. TextView#onDraw() 方法中根據(jù)跑動(dòng)距離移動(dòng)畫(huà)布并繪制首部文本,再根據(jù)跑動(dòng)距離判斷是否可以移動(dòng)畫(huà)布繪制尾部文本

小結(jié)

TextView 通過(guò)移動(dòng)畫(huà)布繪制兩次文本實(shí)現(xiàn)跑馬燈效果,根據(jù)兩幀繪制的時(shí)間差計(jì)算跑動(dòng)距離,怎一個(gè)"妙"了得

應(yīng)用

上面分析完原生 Android TextView 跑馬燈的實(shí)現(xiàn)原理,但是原生 Android TextView 跑馬燈有幾點(diǎn)不足:

  1. 無(wú)法設(shè)置跑動(dòng)速度
  2. 無(wú)法設(shè)置重跑間隔時(shí)長(zhǎng)
  3. 無(wú)法實(shí)現(xiàn)上下跑動(dòng)

以上第1、2點(diǎn)在上面 Marquee 分析中已經(jīng)有解決方案,接下來(lái)根據(jù)原生實(shí)現(xiàn)原理實(shí)現(xiàn)第3點(diǎn)上下跑動(dòng)

MarqueeTextView

這里給出實(shí)現(xiàn)方案,列出主要實(shí)現(xiàn)邏輯,繼承 AppCompatTextView,復(fù)寫(xiě) onDraw() 方法,上下跑動(dòng)主要是計(jì)算上下跑動(dòng)的距離,然后再次重繪 TextView 上下移動(dòng)畫(huà)布繪制文本

/**
 * 繼承AppCompatTextView,復(fù)寫(xiě)onDraw方法
 */
public class MarqueeTextView extends AppCompatTextView {

    private static final int DEFAULT_BG_COLOR = Color.parseColor("#FFEFEFEF");

    @IntDef({HORIZONTAL, VERTICAL})
    @Retention(RetentionPolicy.SOURCE)
    public @interface OrientationMode {
    }

    public static final int HORIZONTAL = 0;
    public static final int VERTICAL = 1;

    private Marquee mMarquee;
    private boolean mRestartMarquee;
    private boolean isMarquee;

    private int mOrientation;

    public MarqueeTextView(@NonNull Context context) {
        this(context, null);
    }

    public MarqueeTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MarqueeTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MarqueeTextView, defStyleAttr, 0);

        mOrientation = ta.getInt(R.styleable.MarqueeTextView_orientation, HORIZONTAL);

        ta.recycle();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        if (mOrientation == HORIZONTAL) {
            if (getWidth() > 0) {
                mRestartMarquee = true;
            }
        } else {
            if (getHeight() > 0) {
                mRestartMarquee = true;
            }
        }
    }

    private void restartMarqueeIfNeeded() {
        if (mRestartMarquee) {
            mRestartMarquee = false;
            startMarquee();
        }
    }

    public void setMarquee(boolean marquee) {
        boolean wasStart = isMarquee();

        isMarquee = marquee;

        if (wasStart != marquee) {
            if (marquee) {
                startMarquee();
            } else {
                stopMarquee();
            }
        }
    }

    public void setOrientation(@OrientationMode int orientation) {
        mOrientation = orientation;
    }

    public int getOrientation() {
        return mOrientation;
    }

    public boolean isMarquee() {
        return isMarquee;
    }

    private void stopMarquee() {
        if (mOrientation == HORIZONTAL) {
            setHorizontalFadingEdgeEnabled(false);
        } else {
            setVerticalFadingEdgeEnabled(false);
        }

        requestLayout();
        invalidate();

        if (mMarquee != null && !mMarquee.isStopped()) {
            mMarquee.stop();
        }
    }

    private void startMarquee() {
        if (canMarquee()) {

            if (mOrientation == HORIZONTAL) {
                setHorizontalFadingEdgeEnabled(true);
            } else {
                setVerticalFadingEdgeEnabled(true);
            }

            if (mMarquee == null) mMarquee = new Marquee(this);
            mMarquee.start(-1);
        }
    }

    private boolean canMarquee() {
        if (mOrientation == HORIZONTAL) {
            int viewWidth = getWidth() - getCompoundPaddingLeft() -
                getCompoundPaddingRight();
            float lineWidth = getLayout().getLineWidth(0);
            return (mMarquee == null || mMarquee.isStopped())
                && (isFocused() || isSelected() || isMarquee())
                && viewWidth > 0
                && lineWidth > viewWidth;
        } else {
            int viewHeight = getHeight() - getCompoundPaddingTop() -
                getCompoundPaddingBottom();
            float textHeight = getLayout().getHeight();
            return (mMarquee == null || mMarquee.isStopped())
                && (isFocused() || isSelected() || isMarquee())
                && viewHeight > 0
                && textHeight > viewHeight;
        }
    }

    /**
     * 仿照TextView#onDraw()方法
     */
    @Override
    protected void onDraw(Canvas canvas) {
        restartMarqueeIfNeeded();

        super.onDraw(canvas);

        // 再次繪制背景色,覆蓋下面由TextView繪制的文本,視情況可以不調(diào)用`super.onDraw(canvas);`
        // 如果沒(méi)有背景色則使用默認(rèn)顏色
        Drawable background = getBackground();
        if (background != null) {
            background.draw(canvas);
        } else {
            canvas.drawColor(DEFAULT_BG_COLOR);
        }

        canvas.save();

        canvas.translate(0, 0);

        // 實(shí)現(xiàn)左右跑馬燈
        if (mOrientation == HORIZONTAL) {
            if (mMarquee != null && mMarquee.isRunning()) {
                final float dx = -mMarquee.getScroll();
                canvas.translate(dx, 0.0F);
            }

            getLayout().draw(canvas, null, null, 0);

            if (mMarquee != null && mMarquee.shouldDrawGhost()) {
                final float dx = mMarquee.getGhostOffset();
                canvas.translate(dx, 0.0F);
                getLayout().draw(canvas, null, null, 0);
            }
        } else {
            // 實(shí)現(xiàn)上下跑馬燈
            if (mMarquee != null && mMarquee.isRunning()) {
                final float dy = -mMarquee.getScroll();
                canvas.translate(0.0F, dy);
            }

            getLayout().draw(canvas, null, null, 0);

            if (mMarquee != null && mMarquee.shouldDrawGhost()) {
                final float dy = mMarquee.getGhostOffset();
                canvas.translate(0.0F, dy);
                getLayout().draw(canvas, null, null, 0);
            }
        }

        canvas.restore();
    }
}

Marquee

private static final class Marquee {
    // 修改此字段設(shè)置重跑時(shí)間間隔 - 對(duì)應(yīng)不足點(diǎn)2
    private static final int MARQUEE_DELAY = 1200;

    // 修改此字段設(shè)置跑動(dòng)速度 - 對(duì)應(yīng)不足點(diǎn)1
    private static final int MARQUEE_DP_PER_SECOND = 30;

    private static final byte MARQUEE_STOPPED = 0x0;
    private static final byte MARQUEE_STARTING = 0x1;
    private static final byte MARQUEE_RUNNING = 0x2;

    private static final String METHOD_GET_FRAME_TIME = "getFrameTime";

    private final WeakReference<MarqueeTextView> mView;
    private final Choreographer mChoreographer;

    private byte mStatus = MARQUEE_STOPPED;
    private final float mPixelsPerSecond;
    private float mMaxScroll;
    private float mMaxFadeScroll;
    private float mGhostStart;
    private float mGhostOffset;
    private float mFadeStop;
    private int mRepeatLimit;

    private float mScroll;
    private long mLastAnimationMs;

    Marquee(MarqueeTextView v) {
        final float density = v.getContext().getResources().getDisplayMetrics().density;
        mPixelsPerSecond = MARQUEE_DP_PER_SECOND * density;
        mView = new WeakReference<>(v);
        mChoreographer = Choreographer.getInstance();
    }

    private final Choreographer.FrameCallback mTickCallback = frameTimeNanos -> tick();

    private final Choreographer.FrameCallback mStartCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            mStatus = MARQUEE_RUNNING;
            mLastAnimationMs = getFrameTime();
            tick();
        }
    };

    /**
     * `getFrameTime`是隱藏api,此處使用反射調(diào)用,高系統(tǒng)版本可能失效,可使用某些方案繞過(guò)此限制
     */
    @SuppressLint("PrivateApi")
    private long getFrameTime() {
        try {
            Class<? extends Choreographer> clz = mChoreographer.getClass();
            Method getFrameTime = clz.getDeclaredMethod(METHOD_GET_FRAME_TIME);
            getFrameTime.setAccessible(true);
            return (long) getFrameTime.invoke(mChoreographer);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    private final Choreographer.FrameCallback mRestartCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            if (mStatus == MARQUEE_RUNNING) {
                if (mRepeatLimit >= 0) {
                    mRepeatLimit--;
                }
                start(mRepeatLimit);
            }
        }
    };

    void tick() {
        if (mStatus != MARQUEE_RUNNING) {
            return;
        }

        mChoreographer.removeFrameCallback(mTickCallback);

        final MarqueeTextView textView = mView.get();
        if (textView != null && (textView.isFocused() || textView.isSelected() || textView.isMarquee())) {
            long currentMs = getFrameTime();
            long deltaMs = currentMs - mLastAnimationMs;
            mLastAnimationMs = currentMs;
            float deltaPx = deltaMs / 1000F * mPixelsPerSecond;
            mScroll += deltaPx;
            if (mScroll > mMaxScroll) {
                mScroll = mMaxScroll;
                mChoreographer.postFrameCallbackDelayed(mRestartCallback, MARQUEE_DELAY);
            } else {
                mChoreographer.postFrameCallback(mTickCallback);
            }
            textView.invalidate();
        }
    }

    void stop() {
        mStatus = MARQUEE_STOPPED;
        mChoreographer.removeFrameCallback(mStartCallback);
        mChoreographer.removeFrameCallback(mRestartCallback);
        mChoreographer.removeFrameCallback(mTickCallback);
        resetScroll();
    }

    private void resetScroll() {
        mScroll = 0.0F;
        final MarqueeTextView textView = mView.get();
        if (textView != null) textView.invalidate();
    }

    void start(int repeatLimit) {
        if (repeatLimit == 0) {
            stop();
            return;
        }
        mRepeatLimit = repeatLimit;
        final MarqueeTextView textView = mView.get();
        if (textView != null && textView.getLayout() != null) {
            mStatus = MARQUEE_STARTING;
            mScroll = 0.0F;

            // 分別計(jì)算左右和上下跑動(dòng)所需的數(shù)據(jù)
            if (textView.getOrientation() == HORIZONTAL) {
                int viewWidth = textView.getWidth() - textView.getCompoundPaddingLeft() -
                    textView.getCompoundPaddingRight();
                float lineWidth = textView.getLayout().getLineWidth(0);
                float gap = viewWidth / 3.0F;
                mGhostStart = lineWidth - viewWidth + gap;
                mMaxScroll = mGhostStart + viewWidth;
                mGhostOffset = lineWidth + gap;
                mFadeStop = lineWidth + viewWidth / 6.0F;
                mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
            } else {
                int viewHeight = textView.getHeight() - textView.getCompoundPaddingTop() -
                    textView.getCompoundPaddingBottom();
                float textHeight = textView.getLayout().getHeight();
                float gap = viewHeight / 3.0F;
                mGhostStart = textHeight - viewHeight + gap;
                mMaxScroll = mGhostStart + viewHeight;
                mGhostOffset = textHeight + gap;
                mFadeStop = textHeight + viewHeight / 6.0F;
                mMaxFadeScroll = mGhostStart + textHeight + textHeight;
            }

            textView.invalidate();
            mChoreographer.postFrameCallback(mStartCallback);
        }
    }

    float getGhostOffset() {
        return mGhostOffset;
    }

    float getScroll() {
        return mScroll;
    }

    float getMaxFadeScroll() {
        return mMaxFadeScroll;
    }

    boolean shouldDrawLeftFade() {
        return mScroll <= mFadeStop;
    }

    boolean shouldDrawTopFade() {
        return mScroll <= mFadeStop;
    }

    boolean shouldDrawGhost() {
        return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
    }

    boolean isRunning() {
        return mStatus == MARQUEE_RUNNING;
    }

    boolean isStopped() {
        return mStatus == MARQUEE_STOPPED;
    }
}

效果

跑馬燈

happy~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容