詳細(xì)分析Fresco源碼之圖片加載流程
一、概述
Fresco 是一個(gè)強(qiáng)大的圖片加載組件。使用它之后,你不需要再去關(guān)心圖片的加載和顯示這些繁瑣的事情! 支持 Android 2.3 及以后的版本。如果需要了解 Fresco 的使用可以訪(fǎng)問(wèn) Fresco 使用文檔。
Fresco是一個(gè)功能完善的圖片加載框架,在Android開(kāi)發(fā)中有著廣泛的應(yīng)用,那么它作為一個(gè)圖片加載框架,有哪些特色讓它備受推崇呢?
- 完善的內(nèi)存管理功能,減少圖片對(duì)內(nèi)存的占用,即便在低端機(jī)器上也有著不錯(cuò)的表現(xiàn)。
- 自定義圖片加載的過(guò)程,可以先顯示低清晰度圖片或者縮略圖,加載完成后再顯示高清圖,可以在加載的時(shí)候縮放和旋轉(zhuǎn)圖片。
- 自定義圖片繪制的過(guò)程,可以自定義谷中焦點(diǎn)、圓角圖、占位圖、overlay、進(jìn)圖條。
- 漸進(jìn)式顯示圖片。
- 支持Gif。
- 支持Webp。
Fresco的組成結(jié)構(gòu)還是比較清晰的,大致如下圖所示:


其實(shí)這兩張圖來(lái)自不同的文章,但是我覺(jué)得兩者的分層實(shí)際上基本是一樣的。只是一個(gè)比較概括,一個(gè)比價(jià)具體,將兩者擺在一起,更有助于大家去理解其實(shí)現(xiàn)細(xì)節(jié)。當(dāng)然除了 UI 和加載顯示部分外,還有 Gif,動(dòng)態(tài)圖片等內(nèi)容,以及對(duì)應(yīng)圖片解碼編碼邏輯等。這部分不打算去講解,因?yàn)檫@部分雖然也是源碼很重要的一部分,但是這部分需要相關(guān)專(zhuān)業(yè)知識(shí)才好說(shuō)明白,此外且涉及到 C++ 代碼。
下面結(jié)合代碼分別解釋一下上面各模塊的作用以及大概的工作原理。
二、DraweeView
它繼承自ImageView,是Fresco加載圖片各個(gè)階段過(guò)程中圖片顯示的載體,比如在加載圖片過(guò)程中它顯示的是占位圖、在加載成功時(shí)切換為目標(biāo)圖片。不過(guò)后續(xù)官方可能不再讓這個(gè)類(lèi)繼承ImageView,所以該類(lèi)并不支持ImageView 的 setImageXxx, setScaleType 以及其他類(lèi)似的方法。目前DraweeView與ImageView唯一的交集是:它利用ImageView來(lái)顯示 Drawable:
//DraweeView.setController()
public void setController(@Nullable DraweeController draweeController) {
mDraweeHolder.setController(draweeController);
super.setImageDrawable(mDraweeHolder.getTopLevelDrawable()); //super 就是 ImageView
}
//DraweeHolder.getTopLevelDrawable()
public @Nullable Drawable getTopLevelDrawable() {
return mHierarchy == null ? null : mHierarchy.getTopLevelDrawable(); // mHierarchy 是 DraweeHierachy,
}
DraweeView.setController()會(huì)在Fresco加載圖片時(shí)會(huì)調(diào)用。其實(shí)在這里可以看出Fresco的圖片顯示原理是 :利用ImageView顯示DraweeHierachy的TopLevelDrawable。上面這段代碼引出了UI 層中另外兩個(gè)關(guān)鍵類(lèi):DraweeHolder和DraweeHierachy。
三、DraweeHierachy
可以說(shuō)它是Fresco圖片顯示的實(shí)現(xiàn)者。它的輸出是Drawable,這個(gè)Drawable會(huì)被DraweeView拿來(lái)顯示(上面已經(jīng)說(shuō)了)。它內(nèi)部有多個(gè)Drawable,當(dāng)前顯示在DraweeView的Drawable叫做TopLevelDrawable。在不同的圖片加載階段,TopLevelDrawable是不同的(比如加載過(guò)程中是 placeholder,加載完成是目標(biāo)圖片)。具體的Drawable切換邏輯是由它來(lái)具體實(shí)現(xiàn)的。
它是由DraweeController直接持有的,因此對(duì)于不同圖片顯示的切換操作具體是由DraweeController來(lái)直接操作的。
四、DraweeHolder
可以把它理解為DraweeView、DraweeHierachy和DraweeController這 3 個(gè)類(lèi)之間的粘合劑, DraweeView 并不直接和DraweeController 和DraweeHierachy 直接接觸,所有的操作都是通過(guò)它傳過(guò)去。這樣,后續(xù)將 DraweeView 的父類(lèi)改為 View,也不會(huì)影響到其他類(lèi)。DraweeView 作為 View 可以感知點(diǎn)擊和生命周期,通過(guò)DraweeHolder 來(lái)控制其他兩個(gè)類(lèi)的操作。
想想如果是你,你會(huì)抽出 DraweeHolder 這樣一個(gè)類(lèi)嗎?實(shí)際上,這里對(duì)我們平時(shí)開(kāi)發(fā)也是有所借鑒,嚴(yán)格控制每一個(gè)類(lèi)之間的關(guān)系,可以引入一些一些中間類(lèi),讓類(lèi)與類(lèi)之間的關(guān)系耦合度降低,方便日后迭代。
具體引用關(guān)系如下圖:

它的主要功能是: 接收DraweeView的圖片加載請(qǐng)求,控制ProducerSequence發(fā)起圖片加載和處理流程,監(jiān)聽(tīng)ProducerSequence加載過(guò)程中的事件(失敗、完成等),并更新最新的Drawable到DraweeHierachy。
五、DraweeController 的構(gòu)造邏輯
在Fresco中DraweeController是通過(guò)PipelineDraweeControllerBuilderSupplier 獲取的。Fresco在初始化時(shí)會(huì)調(diào)用下面的代碼:
// Fresco.java
private static void initializeDrawee(Context context, @Nullable DraweeConfig draweeConfig) {
sDraweeControllerBuilderSupplier = new PipelineDraweeControllerBuilderSupplier(context, draweeConfig);
SimpleDraweeView.initialize(sDraweeControllerBuilderSupplier);
}
sDraweeControllerBuilderSupplier 是靜態(tài)變量,也就是說(shuō)其在只會(huì)初始一次。所有的DraweeController都是通過(guò)調(diào)用sDraweecontrollerbuildersupplier.get() 得到的。
private void init(Context context, @Nullable AttributeSet attrs) {
try {
if (FrescoSystrace.isTracing()) {
FrescoSystrace.beginSection("SimpleDraweeView#init");
}
if (isInEditMode()) {
getTopLevelDrawable().setVisible(true, false);
getTopLevelDrawable().invalidateSelf();
} else {
Preconditions.checkNotNull(
sDraweecontrollerbuildersupplier, "SimpleDraweeView was not initialized!");
mControllerBuilder = sDraweecontrollerbuildersupplier.get(); // 調(diào)用一次就會(huì)創(chuàng)建一個(gè)新的實(shí)例
}
// ...... 省略其他代碼
}
Fresco每次圖片加載都會(huì)對(duì)應(yīng)到一個(gè)DraweeController,一個(gè)DraweeView的多次圖片加載可以復(fù)用同一個(gè)DraweeController:
SimpleDraweeView.java
public void setImageURI(Uri uri, @Nullable Object callerContext) {
DraweeController controller =
mControllerBuilder
.setCallerContext(callerContext)
.setUri(uri) //設(shè)置新的圖片加載路徑
.setOldController(getController()) //復(fù)用 controller
.build();
setController(controller);
}
所以一般情況下 : 一個(gè)DraweeView對(duì)應(yīng)一個(gè)DraweeController。
六、通過(guò) DataSource 發(fā)起圖片加載
在前面已經(jīng)說(shuō)了DraweeController是直接持有DraweeHierachy,所以它觀察到ProducerSequence的數(shù)據(jù)變化是可以很容易更新到DraweeHierachy(具體代碼先不展示了)。那它是如何控制ProducerSequence來(lái)加載圖片的呢?其實(shí)DraweeController并不會(huì)直接和ProducerSequence發(fā)生關(guān)聯(lián)。對(duì)于圖片的加載,它直接接觸的是DataSource,由DataSource進(jìn)而來(lái)控制ProducerSequence發(fā)起圖片加載和處理流程。下面就跟隨源碼來(lái)看一下DraweeController是如果通過(guò)DataSource來(lái)控制ProducerSequence發(fā)起圖片加載和處理流程的。
// AbstractDraweeController.java
protected void submitRequest() {
mDataSource = getDataSource();
final DataSubscriber<T> dataSubscriber = new BaseDataSubscriber<T>() { //可以簡(jiǎn)單的把它理解為一個(gè)監(jiān)聽(tīng)者
@Override
public void onNewResultImpl(DataSource<T> dataSource) { //圖片加載成功
...
}
...
};
...
mDataSource.subscribe(dataSubscriber, mUiThreadImmediateExecutor); //mUiThreadImmediateExecutor是指 dataSubscriber 回調(diào)方法運(yùn)行的線(xiàn)程,這里是主線(xiàn)程
}
那DataSource是什么呢? getDataSource()最終會(huì)調(diào)用到:
// PipelineDraweeControllerBuilder
protected DataSource<CloseableReference<CloseableImage>> getDataSourceForRequest(
DraweeController controller,
String controllerId,
ImageRequest imageRequest,
Object callerContext,
AbstractDraweeControllerBuilder.CacheLevel cacheLevel) {
return mImagePipeline.fetchDecodedImage(
imageRequest,
callerContext,
convertCacheLevelToRequestLevel(cacheLevel),
getRequestListener(controller),
controllerId);
}
// CloseableProducerToDataSourceAdapter<T>
public static <T> DataSource<CloseableReference<T>> create(
Producer<CloseableReference<T>> producer,
SettableProducerContext settableProducerContext,
RequestListener2 listener) {
CloseableProducerToDataSourceAdapter<T> result =
new CloseableProducerToDataSourceAdapter<T>(producer, settableProducerContext, listener);return result;
}
所以DraweeController最終拿到的DataSource是CloseableProducerToDataSourceAdapter。這個(gè)類(lèi)在構(gòu)造的時(shí)候就會(huì)啟動(dòng)圖片加載流程(它的構(gòu)造方法會(huì)調(diào)用producer.produceResults(...),這個(gè)方法就是圖片加載的起點(diǎn),我們后面再看)。
這里我們總結(jié)一下Fresco中DataSource的概念以及作用:在Fresco中DraweeController每發(fā)起一次圖片加載就會(huì)創(chuàng)建一個(gè)DataSource,這個(gè)DataSource用來(lái)提供這次請(qǐng)求的數(shù)據(jù)(圖片)。DataSource只是一個(gè)接口,至于具體的加載流程Fresco是通過(guò)ProducerSequence來(lái)實(shí)現(xiàn)的。
七、Fresco圖片加載前的邏輯
了解了上面的知識(shí)后,我們過(guò)一遍圖片加載的源碼(從 UI 到 DraweeController),來(lái)理一下目前所了解的各個(gè)模塊之間的聯(lián)系。我們?cè)谑褂?code>Fresco加載圖片時(shí)一般是使用這個(gè)API:SimpleDraweeView.setImageURI(imageLink),這個(gè)方法最終會(huì)調(diào)用到:
// SimpleDraweeView.java
public void setImageURI(Uri uri, @Nullable Object callerContext) {
DraweeController controller = mControllerBuilder
.setCallerContext(callerContext)
.setUri(uri)
.setOldController(getController())
.build(); //這里會(huì)復(fù)用 controller
setController(controller);
}
public void setController(@Nullable DraweeController draweeController) {
mDraweeHolder.setController(draweeController);
super.setImageDrawable(mDraweeHolder.getTopLevelDrawable());
}
即每次加載都會(huì)使用DraweeControllerBuilder來(lái)build一個(gè)DraweeController。其實(shí)這個(gè)DraweeController默認(rèn)是復(fù)用的,這里的復(fù)用針對(duì)的是同一個(gè)SimpleDraweeView
。然后會(huì)把DraweeController設(shè)置給DraweeHolder,并在加載開(kāi)始默認(rèn)是從DraweeHolder獲取TopLevelDrawable并展示到DraweeView。繼續(xù)看一下DraweeHolder的邏輯:
// DraweeHolder.java
public @Nullable Drawable getTopLevelDrawable() {
return mHierarchy == null ? null : mHierarchy.getTopLevelDrawable();
}
/** Sets a new controller. */
public void setController(@Nullable DraweeController draweeController) {
boolean wasAttached = mIsControllerAttached;
if (wasAttached) {
detachController();
}
// Clear the old controller
if (isControllerValid()) {
mEventTracker.recordEvent(Event.ON_CLEAR_OLD_CONTROLLER);
mController.setHierarchy(null);
}
mController = draweeController;
// 注意這里是只有確定已經(jīng) attached 才會(huì)調(diào)用,也就是才回去加載圖片
if (wasAttached) {
attachController();
}
}
在DraweeHolder.setController()中把DraweeHierachy設(shè)置給DraweeController,并重新attachController(),attachController()主要調(diào)用了DraweeController.onAttach():
// AbstractDraweeController.java
public void onAttach() {
...
mIsAttached = true;
if (!mIsRequestSubmitted) {
submitRequest();
}
}
protected void submitRequest() {
mDataSource = getDataSource();
final DataSubscriber<T> dataSubscriber = new BaseDataSubscriber<T>() { //可以簡(jiǎn)單的把它理解為一個(gè)監(jiān)聽(tīng)者
@Override
public void onNewResultImpl(DataSource<T> dataSource) { //圖片加載成功
...
}
...
};
...
mDataSource.subscribe(dataSubscriber, mUiThreadImmediateExecutor); //mUiThreadImmediateExecutor是指 dataSubscriber 回調(diào)方法運(yùn)行的線(xiàn)程,這里是主線(xiàn)程
}
即通過(guò)submitRequest()提交了一個(gè)請(qǐng)求,這個(gè)方法我們前面已經(jīng)看過(guò)了,它所做的主要事情就是,構(gòu)造了一個(gè)DataSource。這個(gè)DataSource我們經(jīng)過(guò)追蹤,它的實(shí)例實(shí)際上是CloseableProducerToDataSourceAdapter。CloseableProducerToDataSourceAdapter在構(gòu)造時(shí)就會(huì)調(diào)用producer.produceResults(...),進(jìn)而發(fā)起整個(gè)圖片加載流程。
用下面這張圖總結(jié)從SimpleDraweeView->DraweeController的圖片加載邏輯:

到這里我們梳理完了Fresco在真正發(fā)起圖片加載前所走的邏輯,那么Fresco的圖片加載流程是如何控制的呢?到底經(jīng)歷了哪些步驟呢?
八、Producer
Fresco中有關(guān)圖片的內(nèi)存緩存、解碼、編碼、磁盤(pán)緩存、網(wǎng)絡(luò)請(qǐng)求都是在這一層實(shí)現(xiàn)的,而所有的實(shí)現(xiàn)的基本單元是Producer,所以我們先來(lái)理解一下Producer:
看一下它的定義:
/**
* <p> Execution of image request consists of multiple different tasks such as network fetch,
* disk caching, memory caching, decoding, applying transformations etc. Producer<T> represents
* single task whose result is an instance of T. Breaking entire request into sequence of
* Producers allows us to construct different requests while reusing the same blocks.
*/
public interface Producer<T> {
/**
* Start producing results for given context. Provided consumer is notified whenever progress is made (new value is ready or error occurs).
*/
void produceResults(Consumer<T> consumer, ProducerContext context);
}
結(jié)合注釋我們可以這樣定義Producer的作用:一個(gè)Producer用來(lái)處理整個(gè)Fresco圖片處理流程中的一步,比如從網(wǎng)絡(luò)獲取圖片、內(nèi)存獲取圖片、解碼圖片等等。而對(duì)于Consumer可以把它理解為監(jiān)聽(tīng)者,看一下它的定義:
public interface Consumer<T> {
/**
* Called by a producer whenever new data is produced. This method should not throw an exception.
*
* <p>In case when result is closeable resource producer will close it after onNewResult returns.
* Consumer needs to make copy of it if the resource must be accessed after that. Fortunately,
* with CloseableReferences, that should not impose too much overhead.
*
* @param newResult
* @param status bitwise values describing the returned result
* @see Status for status flags
*/
void onNewResult(T newResult, @Status int status);
/**
* Called by a producer whenever it terminates further work due to Throwable being thrown. This
* method should not throw an exception.
*
* @param t
*/
void onFailure(Throwable t);
/** Called by a producer whenever it is cancelled and won't produce any more results */
void onCancellation();
/**
* Called when the progress updates.
*
* @param progress in range [0, 1]
*/
void onProgressUpdate(float progress);
}
Producer的處理結(jié)果可以通過(guò)Consumer來(lái)告訴外界,比如是失敗還是成功。
九、Producer 的組合
一個(gè)ProducerA可以接收另一個(gè)ProducerB作為參數(shù),如果ProducerA處理完畢后可以調(diào)用ProducerB來(lái)繼續(xù)處理。并傳入Consumer來(lái)觀察ProducerB的處理結(jié)果。比如Fresco在加載圖片時(shí)會(huì)先去內(nèi)存緩存獲取,如果內(nèi)存緩存中沒(méi)有那么就網(wǎng)絡(luò)加載。這里涉及到兩個(gè)Producer分別是BitmapMemoryCacheProducer和NetworkFetchProducer,假設(shè)BitmapMemoryCacheProducer為ProducerA,NetworkFetchProducer為ProducerB。我們用偽代碼看一下他們的邏輯:
// BitmapMemoryCacheProducer.java
public class BitmapMemoryCacheProducer implements Producer<CloseableReference<CloseableImage>> {
private final Producer<CloseableReference<CloseableImage>> mInputProducer;
// 我們假設(shè) inputProducer 在這里為NetworkFetchProducer
public BitmapMemoryCacheProducer(...,Producer<CloseableReference<CloseableImage>> inputProducer) {
...
mInputProducer = inputProducer;
}
@Override
public void produceResults(Consumer<CloseableReference<CloseableImage>> consumer,...) {
CloseableReference<CloseableImage> cachedReference = mMemoryCache.get(cacheKey);
if (cachedReference != null) {
//從緩存中獲取成功,直接通知外界
consumer.onNewResult(cachedReference, BaseConsumer.simpleStatusForIsLast(isFinal));
return;
}
//包了一層Consumer,即mInputProducer產(chǎn)生結(jié)果時(shí),它自己可以觀察到
Consumer<CloseableReference<CloseableImage>> wrappedConsumer = wrapConsumer(consumer..);
//網(wǎng)絡(luò)加載
mInputProducer.produceResults(wrappedConsumer, producerContext);
}
}
// NetworkFetchProducer.java
public class NetworkFetchProducer implements Producer<EncodedImage> {
// 它并沒(méi)有 inputProducer, 對(duì)于 Fresco 的圖片加載來(lái)說(shuō)如果網(wǎng)絡(luò)都獲取失敗,那么就是圖片加載失敗了
@Override
public void produceResults(final Consumer<CloseableReference<CloseableImage>> consumer,..) {
// 網(wǎng)路獲取
// ...
if(獲取到網(wǎng)絡(luò)圖片){
notifyConsumer(...); //把結(jié)果通知給consumer,即觀察者
}
...
}
}
代碼可能不是很好理解,可以結(jié)合下面這張圖來(lái)理解這個(gè)關(guān)系:

Fresco可以通過(guò)組裝多個(gè)不同的Producer來(lái)靈活的定義不同的圖片處理流程的,多個(gè)Producer組裝在一塊稱(chēng)為ProducerSequence (Fresco 中并沒(méi)有這個(gè)類(lèi)哦)。一個(gè)ProducerSequence一般定義一種圖片處理流程,比如網(wǎng)絡(luò)加載圖片的ProducerSequence叫做NetworkFetchSequence,它包含多個(gè)不同類(lèi)型的Producer。
十、網(wǎng)絡(luò)圖片加載的處理流程
在Fresco中不同的圖片請(qǐng)求會(huì)有不同的ProducerSequence來(lái)處理,比如網(wǎng)絡(luò)圖片請(qǐng)求:
// ProducerSequenceFactory.java
private Producer<CloseableReference<CloseableImage>> getBasicDecodedImageSequence(ImageRequest imageRequest) {
switch (imageRequest.getSourceUriType()) {
case SOURCE_TYPE_NETWORK: return getNetworkFetchSequence();
...
}
所以對(duì)于網(wǎng)絡(luò)圖片請(qǐng)求會(huì)調(diào)用getNetworkFetchSequence:
/**
* swallow result if prefetch -> bitmap cache get -> background thread hand-off -> multiplex ->
* bitmap cache -> decode -> multiplex -> encoded cache -> disk cache -> (webp transcode) ->
* network fetch.
*/
private synchronized Producer<CloseableReference<CloseableImage>> getNetworkFetchSequence() {
...
mNetworkFetchSequence = new BitmapCacheGetToDecodeSequence(getCommonNetworkFetchToEncodedMemorySequence());
...
return mNetworkFetchSequence;
}
getNetworkFetchSequence會(huì)經(jīng)過(guò)重重調(diào)用來(lái)組合多個(gè)Producer。這里我就不追代碼邏輯了,直接用下面這張圖來(lái)描述Fresco網(wǎng)絡(luò)加載圖片的處理流程:

可以看到Fresco的整個(gè)圖片加載過(guò)程還是十分復(fù)雜的。并且上圖我只是羅列一些關(guān)鍵的Producer,其實(shí)還有一些我沒(méi)有畫(huà)出來(lái)。
十一、總結(jié)
為了輔助理解,再提供一張總結(jié)的流程圖,將上面整個(gè)過(guò)程都放在里面了。后續(xù)的系列文章會(huì)詳細(xì)介紹 UI 和圖片加載過(guò)程,希望通過(guò)閱讀其源碼來(lái)詳細(xì)了解內(nèi)部的代碼邏輯以及設(shè)計(jì)思路。

其實(shí)我們?cè)陂喿x別人源碼的時(shí)候,除了要知道具體的細(xì)節(jié)之外,也要注意別人的模塊設(shè)計(jì),借鑒其設(shè)計(jì)思想。然后想想如果是你在設(shè)計(jì)的時(shí)候,你會(huì)怎么劃分模塊,如何將不同的模塊聯(lián)系起來(lái)。
當(dāng)模塊劃分后,里面的子模塊又是如何劃分的,它們之間協(xié)作關(guān)系如何保持。
以上就是詳細(xì)分析Fresco源碼之圖片加載流程的詳細(xì)內(nèi)容,更多關(guān)于Fresco 圖片加載流程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android編程實(shí)現(xiàn)手繪及保存為圖片的方法(附demo源碼下載)
這篇文章主要介紹了Android編程實(shí)現(xiàn)手繪及保存為圖片的方法,涉及Android畫(huà)布的使用及圖片的操作技巧,并附帶了demo源碼供讀者下載,需要的朋友可以參考下2015-12-12
Android開(kāi)發(fā)之Parcel機(jī)制實(shí)例分析
這篇文章主要介紹了Android開(kāi)發(fā)之Parcel機(jī)制,實(shí)例分析了Parcel機(jī)制的原理與實(shí)現(xiàn)技巧,需要的朋友可以參考下2015-05-05
Android開(kāi)發(fā)中通過(guò)手機(jī)號(hào)+短信驗(yàn)證碼登錄的實(shí)例代碼
最近在開(kāi)發(fā)一個(gè)android的項(xiàng)目,需要通過(guò)獲取手機(jī)驗(yàn)證碼來(lái)完成登錄功能,接下來(lái)通過(guò)實(shí)例代碼給大家分享手機(jī)號(hào)+短信驗(yàn)證碼登錄的實(shí)現(xiàn)方法,需要的的朋友參考下吧2017-05-05
flutter中的網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)獲取詳解
這篇文章主要為大家介紹了flutter中的網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)獲取示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
Android自定義ViewGroup(側(cè)滑菜單)詳解及簡(jiǎn)單實(shí)例
這篇文章主要介紹了Android自定義ViewGroup(側(cè)滑菜單)詳解及簡(jiǎn)單實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-02-02
android嵌套滾動(dòng)入門(mén)實(shí)踐
嵌套滾動(dòng)是 Android OS 5.0之后,google 為我們提供的新特性,本篇文章主要介紹了android嵌套滾動(dòng)入門(mén)實(shí)踐,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05

