Android?TextView跑馬燈實(shí)現(xiàn)原理及方法實(shí)例
前言
自定義View實(shí)現(xiàn)的跑馬燈一直沒有實(shí)現(xiàn)類似 Android TextView 的跑馬燈首尾相接的效果,所以一直想看看Android TextView 的跑馬燈是如何實(shí)現(xiàn)
本文主要探秘 Android TextView 的跑馬燈實(shí)現(xiàn)原理及實(shí)現(xiàn)自下往上效果的跑馬燈
探秘
TextView#onDraw
原生 Android TextView 如何設(shè)置開啟跑馬燈效果,此處不再描述,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)畫布 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)畫布 canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f); // 繪制尾部文本 layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical); } ? canvas.restore(); }
Marquee
根據(jù) onDraw() 方法分析,跑馬燈效果的實(shí)現(xiàn)主要依賴 mMarquee 這個(gè)對(duì)象來實(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; // 跑馬燈跑完一次后多久開始下一次 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í)候開始繪制 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),用于跑馬燈開始跑動(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)的距離,減輕視覺上跳動(dòng)/卡頓 float deltaPx = deltaMs * mPixelsPerMs; // 計(jì)算跑動(dòng)距離 mScroll += deltaPx; // 判斷是否已經(jīng)跑完 if (mScroll > mMaxScroll) { mScroll = mMaxScroll; // 發(fā)送重新開始跑動(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í)候可以開始繪制尾部文本:首部文本跑動(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(); // 開始跑動(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)原理豁然明亮
- 在 TextView 開啟跑馬燈效果時(shí)調(diào)用 Marquee#start() 方法
- 在 Marquee#start() 方法中觸發(fā) TextView 重繪,開始計(jì)算跑動(dòng)距離
- 在 TextView#onDraw() 方法中根據(jù)跑動(dòng)距離移動(dòng)畫布并繪制首部文本,再根據(jù)跑動(dòng)距離判斷是否可以移動(dòng)畫布繪制尾部文本
小結(jié)
TextView 通過移動(dòng)畫布繪制兩次文本實(shí)現(xiàn)跑馬燈效果,根據(jù)兩幀繪制的時(shí)間差計(jì)算跑動(dòng)距離,怎一個(gè)"妙"字了得
應(yīng)用
上面分析完原生 Android TextView 跑馬燈的實(shí)現(xiàn)原理,但是原生 Android TextView 跑馬燈有幾點(diǎn)不足:
- 無法設(shè)置跑動(dòng)速度
- 無法設(shè)置重跑間隔時(shí)長(zhǎng)
- 無法實(shí)現(xiàn)上下跑動(dòng)
以上第1、2點(diǎn)在上面 Marquee 分析中已經(jīng)有解決方案,接下來根據(jù)原生實(shí)現(xiàn)原理實(shí)現(xiàn)第3點(diǎn)上下跑動(dòng)
MarqueeTextView
這里給出實(shí)現(xiàn)方案,列出主要實(shí)現(xiàn)邏輯,繼承 AppCompatTextView,復(fù)寫 onDraw() 方法,上下跑動(dòng)主要是計(jì)算上下跑動(dòng)的距離,然后再次重繪 TextView 上下移動(dòng)畫布繪制文本
/** * 繼承AppCompatTextView,復(fù)寫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);` // 如果沒有背景色則使用默認(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)版本可能失效,可使用某些方案繞過此限制 */ @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; } }
效果
總結(jié)
到此這篇關(guān)于Android TextView跑馬燈實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Android TextView跑馬燈內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
移動(dòng)端開發(fā)之Jetpack?Hilt技術(shù)實(shí)現(xiàn)解耦
Hilt的出現(xiàn)解決前兩點(diǎn)問題,因?yàn)镠ilt是Dagger針對(duì)Android平臺(tái)的場(chǎng)景化框架,比如Dagger需要我們手動(dòng)聲明注入的地方,而Android聲明的地方不都在onCreate()嗎,所以Hilt就幫我們做了,除此之外還做了很多事情2023-02-02android實(shí)現(xiàn)微信聯(lián)合登錄開發(fā)示例
本篇文章主要介紹了android實(shí)現(xiàn)微信聯(lián)合登錄開發(fā)示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-10-10Flutter實(shí)現(xiàn)固定header底部滑動(dòng)頁(yè)效果示例
這篇文章主要為大家介紹了Flutter實(shí)現(xiàn)固定header底部滑動(dòng)頁(yè)效果示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12flutter升級(jí)3.7.3報(bào)錯(cuò)Unable?to?find?bundled?Java?version解決
這篇文章主要介紹了flutter升級(jí)3.7.3報(bào)錯(cuò)Unable?to?find?bundled?Java?version解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加2023-02-02詳解Android(共享元素)轉(zhuǎn)場(chǎng)動(dòng)畫開發(fā)實(shí)踐
本篇文章主要介紹了詳解Android(共享元素)轉(zhuǎn)場(chǎng)動(dòng)畫開發(fā)實(shí)踐,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-08-08Android入門之AlertDialog用法實(shí)例分析
這篇文章主要介紹了Android入門之AlertDialog用法,對(duì)Android初學(xué)者有很多的借鑒學(xué)習(xí)之處,需要的朋友可以參考下2014-08-08Recyclerview添加頭布局和尾布局、item點(diǎn)擊事件詳解
這篇文章主要為大家詳細(xì)介紹了Recyclerview添加頭布局和尾布局、item點(diǎn)擊事件的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08