欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

flutter圖片組件核心類(lèi)源碼解析

 更新時(shí)間:2023年04月20日 10:08:54   作者:allenymt  
這篇文章主要為大家介紹了flutter圖片組件源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

導(dǎo)語(yǔ)

在使用flutter 自帶圖片組件的過(guò)程中,大家有沒(méi)有考慮過(guò)flutter是如何加載一張網(wǎng)絡(luò)圖片的? 以及對(duì)自帶的圖片組件我們可以做些什么優(yōu)化?

問(wèn)題

flutter 網(wǎng)絡(luò)圖片是怎么請(qǐng)求的?

圖片請(qǐng)求成功后是這么展示的? gif的每一幀是怎么支持展示的?

如何支持圖片的磁盤(pán)緩存?

接下來(lái),讓我們帶著問(wèn)題一起探究flutter 圖片組件的內(nèi)部原理

本文源碼分析以flutter-1.22版本為準(zhǔn),只涉及到dart端,c層圖片解碼不涉及

Image的核心類(lèi)圖及其關(guān)系

自己重新畫(huà)一張

  • Image,是一個(gè)statefulWidget,flutter image的核心入口類(lèi),包含了network,file,assert,memory這幾個(gè)主要的功能,分包對(duì)應(yīng)網(wǎng)絡(luò)圖片,文件圖片,APP內(nèi)置assert圖片,從文件流解析圖片
  • _ImageState,由于Image是statefulWidget,所以核心代碼都在_ImageState
  • ImageStream ,處理圖片資源,ImageState和ImageStreamCompleter的橋梁
  • ImageInfo ,圖片原生信息存儲(chǔ)者
  • ImageStreamCompleter,可以理解為一幀幀解析圖片,并把解析的數(shù)據(jù)回調(diào)給展示方,主要有兩個(gè)實(shí)現(xiàn)類(lèi)
    • OneFrameImageStreamCompleter單幀圖片解析器(貌似沒(méi)在用)
    • MultiFrameImageStreamCompleter多幀圖片解析器,源碼里所有圖片都是默認(rèn)使用這個(gè)了
  • ImageProvider,圖片加載器,不同的加載方式有不同的實(shí)現(xiàn)
    • NetworkImage 網(wǎng)絡(luò)加載圖片
    • MemoryImage 從二進(jìn)制流加載圖片
    • AssetImage 加載asset里的image
    • FileImage 從文件中加載圖片
  • ImageCache ,flutter自帶的圖片緩存,只有內(nèi)存緩存,官方自帶cache ,最大個(gè)數(shù)100,最大內(nèi)存100MB
  • ScrollAwareImageProvider,避免圖片在快速滑動(dòng)中加載

網(wǎng)絡(luò)圖片的加載過(guò)程

// 網(wǎng)絡(luò)圖片
Image.network(imgUrl,  //圖片鏈接
      width: w, 
      height: h),
)

上文中提到過(guò),Image是個(gè)StatefulWidget,那核心邏輯看對(duì)應(yīng)的ImageState,ImageState繼承自State,State的生命周期我們知道,首次初始化時(shí)按InitState()->didChangeDependencies->didUpdateWidget()-> build()順序執(zhí)行

ImageState的InitState沒(méi)做什么,圖片請(qǐng)求的發(fā)起是在didChangeDependencies里做的

// ImageState->didChangeDependencies
@override
void didChangeDependencies() {
    // ios在輔助模式下的配置,不影響主流程,我們不分析
  _updateInvertColors(); 
  
  // 核心方法,開(kāi)始請(qǐng)求解析圖片,從這里開(kāi)始,provier,stream,completer開(kāi)始悉數(shù)登場(chǎng)
  _resolveImage();
    
    // 這個(gè)判斷可以認(rèn)為是,當(dāng)前widget 在tree中是否還是激活狀態(tài)
  if (TickerMode.of(context))
    _listenToStream();
  else
    _stopListeningToStream();

  super.didChangeDependencies();
}

再看ImageState里的_resolveImage方法

void _resolveImage() {
    // ScrollAwareImageProvider代理模式,它本身也是繼承的ImageProvider,
    // 它的功能是防止在快速滾動(dòng)時(shí)加載圖片
  final ScrollAwareImageProvider provider = ScrollAwareImageProvider<dynamic>(
    context: _scrollAwareContext,
    imageProvider: widget.image,
  );
  
  // 這里調(diào)用了ImageProvider的resolve方法,圖片請(qǐng)求的主流程
  final ImageStream newStream =
    provider.resolve(createLocalImageConfiguration(
      context,
      size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null,
    ));
  assert(newStream != null);
  // 對(duì)resolve返回的Stream注冊(cè)監(jiān)聽(tīng),這個(gè)監(jiān)聽(tīng)很重要,決定了后續(xù)的圖片展示(包括gif)
  // 刷新當(dāng)前圖片展示一次,例如幀數(shù),加載狀態(tài)等等
  _updateSourceStream(newStream);
}

我們接著看ImageProvider的resolve方法

// 這方法初次看比較繞,其實(shí)就干了三個(gè)事
// 1. 創(chuàng)建了一個(gè)ImageStream
// 2. 創(chuàng)建一個(gè)Key,key由具體的provider自己實(shí)現(xiàn),這個(gè)key用在后面ImageCache里
// 3. 把接下來(lái)的流程封裝在一個(gè)Zone里,捕獲了同步異常和異步異常,不了解Zone的同學(xué)可以參考我另一篇文章
@nonVirtual
ImageStream resolve(ImageConfiguration configuration) {
  assert(configuration != null);
  final ImageStream stream = createStream(configuration);
  // 創(chuàng)建了key,把后續(xù)的流程封裝在zone里,源碼我不貼了,感興趣的同學(xué)自己看下
  _createErrorHandlerAndKey(
    configuration,
    (T key, ImageErrorListener errorHandler) {
      resolveStreamForKey(configuration, stream, key, errorHandler);
    },
    (T? key, dynamic exception, StackTrace? stack) async {
      await null; // wait an event turn in case a listener has been added to the image stream.
      final _ErrorImageCompleter imageCompleter = _ErrorImageCompleter();
      stream.setCompleter(imageCompleter);
      InformationCollector? collector;
      assert(() {
        collector = () sync* {
          yield DiagnosticsProperty<ImageProvider>('Image provider', this);
          yield DiagnosticsProperty<ImageConfiguration>('Image configuration', configuration);
          yield DiagnosticsProperty<T>('Image key', key, defaultValue: null);
        };
        return true;
      }());
      imageCompleter.setError(
        exception: exception,
        stack: stack,
        context: ErrorDescription('while resolving an image'),
        silent: true, // could be a network error or whatnot
        informationCollector: collector,
      );
    },
  );
  return stream;
}

接著看resolveStreamForKey方法,在1.22里,默認(rèn)的provider都是ScrollAwareImageProvider,ScrollAwareImageProvider重寫(xiě)了resolveStreamForKey,這里有滾動(dòng)控制加載的邏輯,但最終調(diào)用的還是ImageProvier的resolveStreamForKey

// ImageProvier -> resolveStreamForKey
@protected
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
    // streem中已經(jīng)有completer了,從緩存中拿,
  if (stream.completer != null) {
    final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
      key,
      () => stream.completer!,
      onError: handleError,
    );
    assert(identical(completer, stream.completer));
    return;
  }
  
  // 如果是首次,新建一個(gè)completer,然后會(huì)執(zhí)行l(wèi)oad這個(gè)函數(shù),就是putIfAbsent的第二個(gè)入?yún)?
  final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
    key,
    () => load(key, PaintingBinding.instance!.instantiateImageCodec),
    onError: handleError,
  );
  // 賦值,注意這里,后面講圖片展示的時(shí)候會(huì)說(shuō)到這里
  if (completer != null) {
    stream.setCompleter(completer);
  }
}

接著看ImageProvider的load,load方法就是圖片的具體加載方法,不同的provider有不同的實(shí)現(xiàn),此時(shí)我們關(guān)注NetworkImage的Provier里的實(shí)現(xiàn)

// NetworkImage
@override
ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
  // Ownership of this controller is handed off to [_loadAsync]; it is that
  // method's responsibility to close the controller's stream when the image
  // has been loaded or an error is thrown.
  final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
    
    // MultiFrameImageStreamCompleter是多幀解析器,默認(rèn)使用的是就是這個(gè),所以默認(rèn)支持gif
  return MultiFrameImageStreamCompleter(
    codec: _loadAsync(key as NetworkImage, chunkEvents, decode), // 異步加載圖片,我們接著看這個(gè)方法
    chunkEvents: chunkEvents.stream, //加載過(guò)程的回調(diào)
    scale: key.scale,
    debugLabel: key.url,
    informationCollector: () {
      return <DiagnosticsNode>[
        DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
        DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
      ];
    },
  );
}

接著看NetworkImage的_loadAsync

// 這里就很清晰了吧,內(nèi)置的HttpClient去加載
Future<ui.Codec> _loadAsync(
  NetworkImage key,
  StreamController<ImageChunkEvent> chunkEvents,
  image_provider.DecoderCallback decode,
) async {
  try {
    assert(key == this);
    final Uri resolved = Uri.base.resolve(key.url);
    final HttpClientRequest request = await _httpClient.getUrl(resolved);

    headers?.forEach((String name, String value) {
      request.headers.add(name, value);
    });
    final HttpClientResponse response = await request.close();
    if (response.statusCode != HttpStatus.ok) {
        // 請(qǐng)求失敗,報(bào)錯(cuò)
      throw image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);
    }
    
    // 二進(jìn)制流數(shù)據(jù)回調(diào)
    final Uint8List bytes = await consolidateHttpClientResponseBytes(
      response,
      onBytesReceived: (int cumulative, int? total) {
        chunkEvents.add(ImageChunkEvent(
          cumulativeBytesLoaded: cumulative,
          expectedTotalBytes: total,
        ));
      },
    );
    if (bytes.lengthInBytes == 0)
      throw Exception('NetworkImage is an empty file: $resolved');
    //解析二進(jìn)制流
    return decode(bytes);
  } catch (e) {
    // Depending on where the exception was thrown, the image cache may not
    // have had a chance to track the key in the cache at all.
    // Schedule a microtask to give the cache a chance to add the key.
    scheduleMicrotask(() {
      PaintingBinding.instance!.imageCache!.evict(key);
    });
    rethrow;
  } finally {
    chunkEvents.close();
  }
}

至此,第一個(gè)問(wèn)題回答完畢,那當(dāng)圖片數(shù)據(jù)請(qǐng)求成功后,是怎么回調(diào)到ImageState并展示到界面中的呢?

網(wǎng)絡(luò)圖片數(shù)據(jù)的回調(diào)和展示過(guò)程

要看回調(diào)和展示,我們從終點(diǎn)ImageState的build方法開(kāi)始看

// 很容易發(fā)現(xiàn)RawImage,RawImage是實(shí)際渲染圖片的widget,這么說(shuō)其實(shí)也不對(duì),RenderImage才是最終渲染的
// 可以看到RawImage的第一個(gè)參數(shù)_imageInfo?.image,那_imageInfo?.image是什么時(shí)候賦值的?
Widget result = RawImage(
  image: _imageInfo?.image,
  debugImageLabel: _imageInfo?.debugLabel,
  width: widget.width,
  height: widget.height,
  scale: _imageInfo?.scale ?? 1.0,
  color: widget.color,
  colorBlendMode: widget.colorBlendMode,
  fit: widget.fit,
  alignment: widget.alignment,
  repeat: widget.repeat,
  centerSlice: widget.centerSlice,
  matchTextDirection: widget.matchTextDirection,
  invertColors: _invertColors,
  isAntiAlias: widget.isAntiAlias,
  filterQuality: widget.filterQuality,
);

還記得第一部分提到的_updateSourceStream(newStream);方法嗎?在這個(gè)方法里對(duì)ImageStrem設(shè)置了一個(gè)監(jiān)聽(tīng)

// 設(shè)置了監(jiān)聽(tīng)
_imageStream.addListener(_getListener());

// ImageStreamListener
ImageStreamListener _getListener({bool recreateListener = false}) {
  if(_imageStreamListener == null || recreateListener) {
    _lastException = null;
    _lastStack = null;
    _imageStreamListener = ImageStreamListener(
      _handleImageFrame, // 每一幀圖片解析完,代表可以展示這一幀了
      onChunk: widget.loadingBuilder == null ? null : _handleImageChunk, // 圖片加載互調(diào)
      onError: widget.errorBuilder != null // 圖片加載錯(cuò)誤互調(diào)
          ? (dynamic error, StackTrace stackTrace) {
              setState(() {
                _lastException = error;
                _lastStack = stackTrace;
              });
            }
          : null,
    );
  }
  return _imageStreamListener;
}

接著看ImageState的_handleImageFrame

// 很簡(jiǎn)單,就是setState,可以看到這里賦值了_imageInfo
void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
  setState(() {
    _imageInfo = imageInfo;
    _loadingProgress = null;
    _frameNumber = _frameNumber == null ? 0 : _frameNumber + 1;
    _wasSynchronouslyLoaded |= synchronousCall;
  });
}

那么這個(gè)_imageStreamListener 是什么時(shí)候回調(diào)的呢? 還記得第一步加載過(guò)程最后一步的MultiFrameImageStreamCompleter嗎?

// MultiFrameImageStreamCompleter就是支持gif的多幀解析器,還有一個(gè)OneFrameImageStreamCompleter,但已經(jīng)不用了
MultiFrameImageStreamCompleter({
  required Future<ui.Codec> codec,
  required double scale,
  String? debugLabel,
  Stream<ImageChunkEvent>? chunkEvents,
  InformationCollector? informationCollector,
}) : assert(codec != null),
     _informationCollector = informationCollector,
     _scale = scale {
  this.debugLabel = debugLabel;
  // _handleCodecReady就是圖片加載完的回調(diào),我們看看他內(nèi)部干了什么
  codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
    // 捕獲錯(cuò)誤并上報(bào)
  });
  // 監(jiān)聽(tīng)回調(diào)
  if (chunkEvents != null) {
    chunkEvents.listen(reportImageChunkEvent,
      onError: (dynamic error, StackTrace stack) {
        reportError(
          context: ErrorDescription('loading an image'),
          exception: error,
          stack: stack,
          informationCollector: informationCollector,
          silent: true,
        );
      },
    );
  }
}

這里回答了第二個(gè)問(wèn)題,gif的每幀是怎么支持的,關(guān)鍵就是MultiFrameImageStreamCompleter這個(gè)類(lèi), 接著看MultiFrameImageStreamCompleter的_handleCodecReady

void _handleCodecReady(ui.Codec codec) {
  _codec = codec;
  assert(_codec != null);

  if (hasListeners) {
      // 看函數(shù)名就知道了,解析下一幀并執(zhí)行
    _decodeNextFrameAndSchedule();
  }
}

MultiFrameImageStreamCompleter的_decodeNextFrameAndSchedule()

Future<void> _decodeNextFrameAndSchedule() async {
  try {
      // 獲得下一幀,這一步在C中處理
    _nextFrame = await _codec!.getNextFrame();
  } catch (exception, stack) {
    reportError(
      context: ErrorDescription('resolving an image frame'),
      exception: exception,
      stack: stack,
      informationCollector: _informationCollector,
      silent: true,
    );
    return;
  }
  // 幀數(shù)不等于1,說(shuō)明圖片有多幀
  if (_codec!.frameCount == 1) {
    // This is not an animated image, just return it and don't schedule more
    // frames.
    _emitFrame(ImageInfo(image: _nextFrame!.image, scale: _scale, debugLabel: debugLabel));
    return;
  }
  // 如果只有一幀,_scheduleAppFrame最終也會(huì)走到_emitFrame
  _scheduleAppFrame();
}

接著看MultiFrameImageStreamCompleter的_emitFrame

// 調(diào)用了setImage
void _emitFrame(ImageInfo imageInfo) {
  setImage(imageInfo);
  _framesEmitted += 1;
}

ImageStreamCompleter的setImage

@protected
void setImage(ImageInfo image) {
  _currentImage = image;
  if (_listeners.isEmpty)
    return;
  // Make a copy to allow for concurrent modification.
  final List<ImageStreamListener> localListeners =
      List<ImageStreamListener>.from(_listeners);
  for (final ImageStreamListener listener in localListeners) {
    try {
        // 在這里回調(diào)了onImage,那這個(gè)回調(diào)是哪里注冊(cè)的呢? 回到ImageStream的addLister里
      listener.onImage(image, false);
    } catch (exception, stack) {
    }
  }
}

ImageStream的addLister里

void addListener(ImageStreamListener listener) {
    // 這里破案了,_completer 不為null的時(shí)候,注冊(cè)了回調(diào),而ImageStream的completer在ImageStream被創(chuàng)建的還是就賦值了
    // 所以前面的listener.onImage(image, false);最終會(huì)回調(diào)到ImageState里的_imageStreamListener
  if (_completer != null)
    return _completer!.addListener(listener);
  _listeners ??= <ImageStreamListener>[];
  _listeners!.add(listener);
}

至此,圖片的是展示流程也分析完畢,第二個(gè)問(wèn)題也回答完了。

補(bǔ)上圖片內(nèi)存緩存的源碼分析

首先要說(shuō)明的是,flutter內(nèi)存緩存默認(rèn)只有內(nèi)存緩存,也就意味著如果殺進(jìn)程重啟,圖片就需要重新加載了。

1.22的內(nèi)存緩存主要分三部分,相比1.17增加了一部分

_pendingImages 正在加載中的緩存,這個(gè)有什么作用呢? 假設(shè)Widget1加載了圖片A,Widget2也在這個(gè)時(shí)候加載了圖片A,那這時(shí)候Widget就復(fù)用了這個(gè)加載中的緩存

_cache 已經(jīng)加載成功的圖片緩存,這個(gè)很好理解

_liveImages 存活的圖片緩存,看代碼主要是在CacheImage之外再加一層緩存,在CacheImage被清楚后,

對(duì)于一張圖片,當(dāng)首次加載時(shí),首先會(huì)在_pendingImages中,注意此時(shí)圖片還未加載成功,所以如果有復(fù)用的情況,會(huì)命中_pendingImages,當(dāng)圖片請(qǐng)求成功后,在_cache和_liveImages都會(huì)保存一份,此時(shí)_pendingImages會(huì)移除。 當(dāng)超過(guò)緩存中的最大數(shù)時(shí),會(huì)從_cache里按照LRU的規(guī)則刪除

如何支持圖片的磁盤(pán)緩存

在看完整個(gè)流程后,對(duì)磁盤(pán)緩存應(yīng)該也有思路了。第一個(gè)是可以自定義ImageProvider,在圖片數(shù)據(jù)請(qǐng)求成功后寫(xiě)入磁盤(pán)緩存,不過(guò)對(duì)于混合項(xiàng)目來(lái)說(shuō),更好的方式應(yīng)該是替換圖片的網(wǎng)絡(luò)請(qǐng)求方式,利用channel和原生(Android ,ios)的圖片庫(kù)加載圖片,這樣可以復(fù)用原生圖片庫(kù)的磁盤(pán)緩存,但也有缺陷,在效率上會(huì)有降低,畢竟多了內(nèi)存的多次拷貝和channel通信。

總結(jié)

本文只是分析了Image.Network的加載和展示過(guò)程,而且也只是涉及到了dart端代碼??偟膩?lái)說(shuō),整個(gè)流程并不復(fù)雜,其他諸如Image.Memory,Image.File 原理都是一樣的,區(qū)別只是各自的ImageProvider不一樣,我們也可以自定義ImageProvider實(shí)現(xiàn)自己想要的效果。

以上就是flutter圖片組件核心類(lèi)源碼解析的詳細(xì)內(nèi)容,更多關(guān)于flutter圖片組件核心類(lèi)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Android webview與js交換JSON對(duì)象數(shù)據(jù)示例

    Android webview與js交換JSON對(duì)象數(shù)據(jù)示例

    js主動(dòng)調(diào)用android的對(duì)象方式,android也無(wú)法返回給js一個(gè)jsonobject,需要js做一下轉(zhuǎn)換,具體代碼如下,感興趣的朋友可以參考下哈
    2013-06-06
  • Android CardView+ViewPager實(shí)現(xiàn)ViewPager翻頁(yè)動(dòng)畫(huà)的方法

    Android CardView+ViewPager實(shí)現(xiàn)ViewPager翻頁(yè)動(dòng)畫(huà)的方法

    本篇文章主要介紹了Android CardView+ViewPager實(shí)現(xiàn)ViewPager翻頁(yè)動(dòng)畫(huà)的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-06-06
  • Android inflater 用法及不同點(diǎn)

    Android inflater 用法及不同點(diǎn)

    在 實(shí)際開(kāi)發(fā)中LayoutInflater這個(gè)類(lèi)還是非常有用的,它的作用類(lèi)似于findViewById()。這篇文章主要介紹了Android inflater 用法,需要的朋友可以參考下
    2018-11-11
  • 2021最新Android筆試題總結(jié)美團(tuán)Android崗職能要求

    2021最新Android筆試題總結(jié)美團(tuán)Android崗職能要求

    這篇文章主要介紹了2021最新Android筆試題總結(jié)以及美團(tuán)Android崗職能要求,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-08-08
  • Android編程ViewPager回彈效果實(shí)例分析

    Android編程ViewPager回彈效果實(shí)例分析

    這篇文章主要介紹了Android編程ViewPager回彈效果,以實(shí)例形式較為詳細(xì)的分析了ViewPager回彈效果的相關(guān)使用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下
    2015-10-10
  • Android實(shí)現(xiàn)簡(jiǎn)單的下拉刷新控件

    Android實(shí)現(xiàn)簡(jiǎn)單的下拉刷新控件

    這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)簡(jiǎn)單的下拉刷新控件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-09-09
  • Android Studio使用Kotlin時(shí),修改代碼后運(yùn)行不生效的解決方法

    Android Studio使用Kotlin時(shí),修改代碼后運(yùn)行不生效的解決方法

    這篇文章主要介紹了Android Studio使用Kotlin時(shí),修改代碼后運(yùn)行不生效的解決方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2020-03-03
  • android開(kāi)發(fā)中ListView與Adapter使用要點(diǎn)介紹

    android開(kāi)發(fā)中ListView與Adapter使用要點(diǎn)介紹

    項(xiàng)目用到ListView,由于要用到 ImageView ,圖片源不是在資源里面的,沒(méi)法使用資源 ID,因此無(wú)法直接使用SimpleAdapter,要自己寫(xiě)一個(gè)Adapter。 在使用ListView和Adapter需要注意以下幾點(diǎn)
    2013-06-06
  • Android ListView的item中嵌套ScrollView的解決辦法

    Android ListView的item中嵌套ScrollView的解決辦法

    有時(shí)候,listview 的item要顯示的字段比較多,考慮到顯示問(wèn)題,item外面不得不嵌套ScrollView來(lái)實(shí)現(xiàn),糾結(jié)怎么解決此問(wèn)題呢?下面小編給大家分享下Android ListView的item中嵌套ScrollView的解決辦法,感興趣的朋友一起看看吧
    2016-10-10
  • Android實(shí)現(xiàn)圖片設(shè)置圓角形式

    Android實(shí)現(xiàn)圖片設(shè)置圓角形式

    這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)圖片設(shè)置圓角形式,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-11-11

最新評(píng)論