提交 31b60a4b 编写于 作者: S Sam Judd

Add support for transformations.

上级 39962a2e
package com.bumptech.glide.loader.bitmap;
import com.bumptech.glide.resize.load.Transformation;
import org.junit.Before;
import org.junit.Test;
import static junit.framework.TestCase.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ImageVideoBitmapLoadFactoryTest {
// Not magic, just a non 0 number.
private static final int IMAGE_SIDE = 200;
private static final String TRANSFORMATION_ID = "id";
private Transformation transformation;
@Before
public void setUp() throws Exception {
transformation = mock(Transformation.class);
when(transformation.getId()).thenReturn(TRANSFORMATION_ID);
}
@Test
public void testIgnoresNullImageLoadFactory() {
ImageVideoBitmapLoadFactory factory = new ImageVideoBitmapLoadFactory(null,
mock(ResourceBitmapLoadFactory.class),
transformation);
assertNotNull(factory.getLoadTask(new Object(), IMAGE_SIDE, IMAGE_SIDE));
}
@Test
public void testIgnoresNullVideoLoadFactory() {
ImageVideoBitmapLoadFactory factory = new ImageVideoBitmapLoadFactory(mock(ResourceBitmapLoadFactory.class),
null, transformation);
assertNotNull(factory.getLoadTask(new Object(), IMAGE_SIDE, IMAGE_SIDE));
}
@Test(expected = IllegalArgumentException.class)
public void testThrowsIfNullVideoAndNullImageLoaders() {
new ImageVideoBitmapLoadFactory(null, null, transformation);
}
@Test(expected = IllegalArgumentException.class)
public void testThrowsWithNullTransformationLoader() {
new ImageVideoBitmapLoadFactory(mock(ResourceBitmapLoadFactory.class),
mock(ResourceBitmapLoadFactory.class), null);
}
}
......@@ -4,6 +4,7 @@ import android.os.Handler;
import com.bumptech.glide.loader.bitmap.resource.ResourceFetcher;
import com.bumptech.glide.resize.cache.DiskCache;
import com.bumptech.glide.resize.cache.ResourceCache;
import com.bumptech.glide.resize.load.Transformation;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
......@@ -40,6 +41,7 @@ public class DefaultResourceRunnerFactoryTest {
ExecutorService service = mock(ExecutorService.class);
ResourceCallback<Object> cb = mock(ResourceCallback.class);
ResourceReferenceCounter resourceReferenceCounter = mock(ResourceReferenceCounter.class);
Transformation<Object> transformation = mock(Transformation.class);
int width = 100;
int height = 100;
......@@ -53,7 +55,8 @@ public class DefaultResourceRunnerFactoryTest {
Metadata metadata = mock(Metadata.class);
public ResourceRunner build() {
return factory.build(ID, width, height, cacheDecoder, fetcher, decoder, encoder, metadata, listener, cb);
return factory.build(ID, width, height, cacheDecoder, fetcher, decoder, transformation, encoder, metadata,
listener, cb);
}
}
}
......@@ -2,6 +2,7 @@ package com.bumptech.glide.resize;
import com.bumptech.glide.loader.bitmap.resource.ResourceFetcher;
import com.bumptech.glide.resize.cache.ResourceCache;
import com.bumptech.glide.resize.load.Transformation;
import org.junit.Before;
import org.junit.Test;
......@@ -176,6 +177,7 @@ public class EngineTest {
Resource<Object> resource = mock(Resource.class);
Map<String, ResourceRunner> runners = new HashMap<String, ResourceRunner>();
ResourceReferenceCounter resourceReferenceCounter = mock(ResourceReferenceCounter.class);
Transformation transformation = mock(Transformation.class);
int width = 100;
int height = 100;
......@@ -192,12 +194,13 @@ public class EngineTest {
engine = new Engine(factory, cache, runners, resourceReferenceCounter);
when(factory.build(eq(ID), eq(width), eq(height), eq(cacheDecoder), eq(fetcher), eq(decoder), eq(encoder),
eq(metadata), eq(engine), eq(cb))).thenReturn(runner);
when(factory.build(eq(ID), eq(width), eq(height), eq(cacheDecoder), eq(fetcher), eq(decoder),
eq(transformation), eq(encoder), eq(metadata), eq(engine), eq(cb))).thenReturn(runner);
}
public Engine.LoadStatus doLoad() {
return engine.load(ID, width, height, cacheDecoder, fetcher, decoder, encoder, metadata, cb);
return engine.load(ID, width, height, cacheDecoder, fetcher, decoder, transformation, encoder, metadata,
cb);
}
}
}
......@@ -2,10 +2,9 @@ package com.bumptech.glide.resize;
import com.bumptech.glide.loader.bitmap.resource.ResourceFetcher;
import com.bumptech.glide.resize.cache.DiskCache;
import com.bumptech.glide.resize.load.Transformation;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
......@@ -18,7 +17,6 @@ import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isNull;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
......@@ -61,6 +59,7 @@ public class SourceResourceRunnerTest {
verify(harness.cb).onResourceReady(eq(harness.result));
}
@Test
public void testResourceIsWrittenToCacheIfFetchedAndDecoded() throws Exception {
InputStream is = new ByteArrayInputStream(new byte[0]);
......@@ -68,20 +67,54 @@ public class SourceResourceRunnerTest {
when(harness.decoder.decode(eq(is), eq(harness.width), eq(harness.height))).thenReturn(harness.result);
final OutputStream expected = new ByteArrayOutputStream();
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
DiskCache.Writer writer = (DiskCache.Writer) invocation.getArguments()[1];
writer.write(expected);
return null;
}
}).when(harness.diskCache).put(eq(ID), any(DiskCache.Writer.class));
harness.runner.run();
harness.runner.write(expected);
verify(harness.encoder).encode(eq(harness.result), eq(expected));
}
@Test
public void testResourceIsTransformedBeforeBeingWrittenToCache() throws Exception {
InputStream is = new ByteArrayInputStream(new byte[0]);
when(harness.fetcher.loadResource(eq(harness.metadata))).thenReturn(is);
when(harness.decoder.decode(eq(is), eq(harness.width), eq(harness.height))).thenReturn(harness.result);
Resource transformed = mock(Resource.class);
when(harness.transformation.transform(eq(harness.result), eq(harness.width), eq(harness.height)))
.thenReturn(transformed);
OutputStream expected = new ByteArrayOutputStream();
harness.runner.run();
harness.runner.write(expected);
verify(harness.encoder).encode(eq(transformed), eq(expected));
}
@Test
public void testDecodedResourceIsRecycledIfTransformedResourceIsDifferent() throws Exception {
InputStream is = new ByteArrayInputStream(new byte[0]);
when(harness.fetcher.loadResource(eq(harness.metadata))).thenReturn(is);
when(harness.decoder.decode(eq(is), eq(harness.width), eq(harness.height))).thenReturn(harness.result);
Resource transformed = mock(Resource.class);
when(harness.transformation.transform(eq(harness.result), eq(harness.width), eq(harness.height)))
.thenReturn(transformed);
harness.runner.run();
verify(harness.result).recycle();
}
@Test
public void testDecodedResourceIsNotRecycledIfResourceIsNotTransformed() throws Exception {
InputStream is = new ByteArrayInputStream(new byte[0]);
when(harness.fetcher.loadResource(eq(harness.metadata))).thenReturn(is);
when(harness.decoder.decode(eq(is), eq(harness.width), eq(harness.height))).thenReturn(harness.result);
harness.runner.run();
verify(harness.result, never()).recycle();
}
@Test
public void testCallbackIsCalledIfFetchFails() throws Exception {
Exception expected = new Exception("Test");
......@@ -135,9 +168,14 @@ public class SourceResourceRunnerTest {
Metadata metadata = mock(Metadata.class);
ResourceCallback<Object> cb = mock(ResourceCallback.class);
Resource<Object> result = mock(Resource.class);
Transformation<Object> transformation = mock(Transformation.class);
int width = 150;
int height = 200;
SourceResourceRunner<Object, Object> runner = new SourceResourceRunner<Object, Object>(ID, width, height,
fetcher, decoder, encoder, diskCache, metadata, cb);
fetcher, decoder, transformation, encoder, diskCache, metadata, cb);
public SourceResourceHarness() {
when(transformation.transform(eq(result), eq(width), eq(height))).thenReturn(result);
}
}
}
package com.bumptech.glide.resize.load;
import android.graphics.Bitmap;
import com.bumptech.glide.resize.bitmap_recycle.BitmapPool;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
import java.util.ArrayList;
import java.util.List;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(RobolectricTestRunner.class)
public class ImageVideoBitmapLoadTaskTest {
private static final int IMAGE_SIDE = 235;
@Test
public void testLoadsOnlyWithImageLoaderIfImageLoaderSucceeds() throws Exception {
Bitmap expected = Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8);
BitmapLoad imageLoad = mock(BitmapLoad.class);
when(imageLoad.load(any(BitmapPool.class))).thenReturn(expected);
Bitmap notExpected = Bitmap.createBitmap(11, 11, Bitmap.Config.ARGB_8888);
BitmapLoad videoLoad = mock(BitmapLoad.class);
when(videoLoad.load(any(BitmapPool.class))).thenReturn(notExpected);
Transformation transformation = mock(Transformation.class);
when(transformation.transform(any(Bitmap.class), any(BitmapPool.class), anyInt(), anyInt()))
.thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
return invocation.getArguments()[0];
}
});
ImageVideoBitmapLoad task = new ImageVideoBitmapLoad(imageLoad, videoLoad,
IMAGE_SIDE, IMAGE_SIDE, transformation);
Bitmap result = task.load(mock(BitmapPool.class));
assertEquals(expected, result);
verify(videoLoad, never()).load(any(BitmapPool.class));
}
@Test
public void testLoadsWithImageLoaderIfVideoLoaderFails() throws Exception {
Bitmap expected = Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8);
ImageVideoBitmapLoad task = createBaseBitmapLoadTask(expected, null);
Bitmap result = task.load(mock(BitmapPool.class));
assertEquals(expected, result);
}
@Test
public void testLoadsWithVideoLoaderIfImageLoadFails() throws Exception {
Bitmap expected = Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8);
ImageVideoBitmapLoad task = createBaseBitmapLoadTask(null, expected);
Bitmap result = task.load(mock(BitmapPool.class));
assertEquals(expected, result);
}
@Test
public void testReturnsNullIfImageAndVideoLoadsFail() throws Exception {
ImageVideoBitmapLoad task = createBaseBitmapLoadTask(null, null);
Bitmap result = task.load(mock(BitmapPool.class));
assertEquals(null, result);
}
@Test
public void testTransformsImageResult() throws Exception {
Bitmap fromImage = Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8);
Bitmap transformed = Bitmap.createBitmap(9, 9, Bitmap.Config.ARGB_8888);
Transformation transformation = mock(Transformation.class);
when(transformation.transform(eq(fromImage), any(BitmapPool.class), anyInt(), anyInt()))
.thenReturn(transformed);
ImageVideoBitmapLoad task = createBaseBitmapLoadTask(fromImage, null, transformation);
Bitmap result = task.load(mock(BitmapPool.class));
assertEquals(transformed, result);
}
@Test
public void testTransformsVideoResult() throws Exception {
Bitmap fromVideo = Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8);
Bitmap transformed = Bitmap.createBitmap(9, 9, Bitmap.Config.ARGB_8888);
Transformation transformation = mock(Transformation.class);
when(transformation.transform(eq(fromVideo), any(BitmapPool.class), anyInt(), anyInt()))
.thenReturn(transformed);
ImageVideoBitmapLoad task = createBaseBitmapLoadTask(null, fromVideo, transformation);
Bitmap result = task.load(mock(BitmapPool.class));
assertEquals(transformed, result);
}
@Test
public void testTransformationIsGivenImageSize() throws Exception {
Transformation transformation = mock(Transformation.class);
int width = 123;
int height = 456;
ImageVideoBitmapLoad task = createBaseBitmapLoadTask(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8),
Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), transformation, width, height);
task.load(mock(BitmapPool.class));
verify(transformation).transform(any(Bitmap.class), any(BitmapPool.class), eq(width), eq(height));
}
@Test
public void testGeneratesIdWithAllArguments() {
List<String> allIds = new ArrayList<String>();
String initialId = getGeneratedIdWithMutatedArgumentIdAt(-1);
for (int i = 0; i < 2; i++) {
allIds.add(getGeneratedIdWithMutatedArgumentIdAt(i));
}
for (String id : allIds) {
assertFalse(initialId.equals(id));
}
}
private ImageVideoBitmapLoad createBaseBitmapLoadTask(Bitmap imageDecoderResult, Bitmap videoDecoderResult)
throws Exception {
return createBaseBitmapLoadTask(imageDecoderResult, videoDecoderResult, null);
}
private ImageVideoBitmapLoad createBaseBitmapLoadTask(Bitmap imageDecoderResult,
Bitmap videoDecoderResult, Transformation transformation) throws Exception {
return createBaseBitmapLoadTask(imageDecoderResult, videoDecoderResult, transformation, IMAGE_SIDE, IMAGE_SIDE);
}
private ImageVideoBitmapLoad createBaseBitmapLoadTask(Bitmap imageDecoderResult, Bitmap videoDecoderResult,
Transformation transformation, int width, int height) throws Exception {
BitmapLoad imageLoad = mock(BitmapLoad.class);
when(imageLoad.load(any(BitmapPool.class))).thenReturn(imageDecoderResult);
BitmapLoad videoLoad = mock(BitmapLoad.class);
when(videoLoad.load(any(BitmapPool.class))).thenReturn(videoDecoderResult);
if (transformation == null) {
transformation = mock(Transformation.class);
when(transformation.transform(any(Bitmap.class), any(BitmapPool.class), anyInt(), anyInt()))
.thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
return invocation.getArguments()[0];
}
});
}
return new ImageVideoBitmapLoad(imageLoad, videoLoad, width, height, transformation);
}
private String getGeneratedIdWithMutatedArgumentIdAt(int argumentIndex) {
String imageLoadId = "ImageLoadId" + (argumentIndex == 0 ? "1" : "");
BitmapLoad imageLoad = mock(BitmapLoad.class);
when(imageLoad.getId()).thenReturn(imageLoadId);
String videoLoadId = "VideoLoadId" + (argumentIndex == 1 ? "1" : "");
BitmapLoad videoLoad = mock(BitmapLoad.class);
when(videoLoad.getId()).thenReturn(videoLoadId);
String transformationId = "TransformationId" + (argumentIndex == 2 ? "1" : "");
Transformation transformation = mock(Transformation.class);
when(transformation.getId()).thenReturn(transformationId);
ImageVideoBitmapLoad task = new ImageVideoBitmapLoad(imageLoad, videoLoad, IMAGE_SIDE, IMAGE_SIDE,
transformation);
return task.getId();
}
}
......@@ -18,6 +18,7 @@ import com.bumptech.glide.resize.ResourceCallback;
import com.bumptech.glide.resize.ResourceDecoder;
import com.bumptech.glide.resize.ResourceEncoder;
import com.bumptech.glide.resize.load.DecodeFormat;
import com.bumptech.glide.resize.load.Transformation;
import com.bumptech.glide.resize.target.Target;
import org.junit.Before;
import org.junit.Test;
......@@ -113,16 +114,16 @@ public class BitmapRequestTest {
request.onSizeReady(100, 100);
verify(harness.engine).load(anyString(), anyInt(), anyInt(), any(ResourceDecoder.class),
any(ResourceFetcher.class), any(ResourceDecoder.class), any(ResourceEncoder.class), eq(expected),
any(ResourceCallback.class));
any(ResourceFetcher.class), any(ResourceDecoder.class), any(Transformation.class),
any(ResourceEncoder.class), eq(expected), any(ResourceCallback.class));
}
@Test
public void testEngineLoadCancelledOnCancel() {
Engine.LoadStatus loadStatus = mock(Engine.LoadStatus.class);
when(harness.engine.load(anyString(), anyInt(), anyInt(), any(ResourceDecoder.class), any(ResourceFetcher.class),
any(ResourceDecoder.class), any(ResourceEncoder.class), any(Metadata.class),
any(ResourceCallback.class))).thenReturn(loadStatus);
when(harness.engine.load(anyString(), anyInt(), anyInt(), any(ResourceDecoder.class),
any(ResourceFetcher.class), any(ResourceDecoder.class), any(Transformation.class),
any(ResourceEncoder.class), any(Metadata.class), any(ResourceCallback.class))).thenReturn(loadStatus);
BitmapRequest request = harness.getBuilder().build();
......
......@@ -11,6 +11,8 @@ import com.bumptech.glide.resize.Metadata;
import com.bumptech.glide.resize.Priority;
import com.bumptech.glide.resize.RequestContext;
import com.bumptech.glide.resize.ResourceDecoder;
import com.bumptech.glide.resize.bitmap.CenterCrop;
import com.bumptech.glide.resize.bitmap.FitCenter;
import com.bumptech.glide.resize.load.BitmapDecoder;
import com.bumptech.glide.resize.load.DecodeFormat;
import com.bumptech.glide.resize.load.Downsampler;
......@@ -41,7 +43,7 @@ import java.util.List;
*/
public class GenericRequestBuilder<ModelType, ImageResourceType, VideoResourceType> {
protected final Context context;
private final List<Transformation> transformations = new ArrayList<Transformation>();
private final List<Transformation<Bitmap>> transformations = new ArrayList<Transformation<Bitmap>>();
private final ModelType model;
private final RequestContext requestContext;
private final Class<ImageResourceType> imageType;
......@@ -75,13 +77,6 @@ public class GenericRequestBuilder<ModelType, ImageResourceType, VideoResourceTy
this.requestContext = requestContext;
this.model = model;
// if (model != null && imageLoader == null && videoLoader == null) {
// throw new NullPointerException("No ModelLoaders given or registered for model class="
// + model.getClass());
// }
// this.imageLoader = imageLoader;
// this.videoLoader = videoLoader;
}
/**
......@@ -235,24 +230,23 @@ public class GenericRequestBuilder<ModelType, ImageResourceType, VideoResourceTy
}
/**
* Transform images using {@link Transformation#CENTER_CROP}.
* Transform images using {@link CenterCrop}.
*
* @return This RequestBuilder
*/
public GenericRequestBuilder<ModelType, ImageResourceType, VideoResourceType> centerCrop() {
return transform(Transformation.CENTER_CROP);
return transform(new CenterCrop(Glide.get(context).getBitmapPool()));
}
/**
* Transform images using {@link Transformation#FIT_CENTER}.
* Transform images using {@link FitCenter}.
*
* @return This RequestBuilder
*/
public GenericRequestBuilder<ModelType, ImageResourceType, VideoResourceType> fitCenter() {
return transform(Transformation.FIT_CENTER);
return transform(new FitCenter(Glide.get(context).getBitmapPool()));
}
/**
* Transform images with the given {@link Transformation}. Appends this transformation onto any existing
* transformations
......@@ -261,7 +255,7 @@ public class GenericRequestBuilder<ModelType, ImageResourceType, VideoResourceTy
* @return This RequestBuilder
*/
public GenericRequestBuilder<ModelType, ImageResourceType, VideoResourceType> transform(
Transformation transformation) {
Transformation<Bitmap> transformation) {
transformations.add(transformation);
return this;
......@@ -463,7 +457,7 @@ public class GenericRequestBuilder<ModelType, ImageResourceType, VideoResourceTy
}
private <Y extends Target, Z> BitmapRequestBuilder<ModelType, Z> buildBitmapRequestForType(Y target,
Class <Z> resourceClass, float sizeMultiplier, Priority priority, RequestCoordinator requestCoordinator) {
Class<Z> resourceClass, float sizeMultiplier, Priority priority, RequestCoordinator requestCoordinator) {
return new BitmapRequestBuilder<ModelType, Z>(resourceClass)
.setContext(context)
.setPriority(priority)
......@@ -482,17 +476,19 @@ public class GenericRequestBuilder<ModelType, ImageResourceType, VideoResourceTy
.setErrorDrawable(errorPlaceholder)
.setSizeMultiplier(sizeMultiplier)
.setPriority(priority)
.setTransformation(getFinalTransformation())
.setRequestCoordinator(requestCoordinator);
}
private Transformation getFinalTransformation() {
@SuppressWarnings("unchecked")
private Transformation<Bitmap> getFinalTransformation() {
switch (transformations.size()) {
case 0:
return Transformation.NONE;
case 1:
return transformations.get(0);
default:
return new MultiTransformation(transformations);
return new MultiTransformation<Bitmap>(transformations);
}
}
}
package com.bumptech.glide.loader.bitmap;
import com.bumptech.glide.loader.bitmap.model.ModelLoader;
import com.bumptech.glide.resize.load.BitmapDecoder;
import com.bumptech.glide.resize.load.BitmapLoad;
import com.bumptech.glide.resize.load.ImageVideoBitmapLoad;
import com.bumptech.glide.resize.load.Transformation;
/**
* A base {@link BitmapLoadFactory} that composes {@link ModelLoader} and {@link BitmapDecoder} sub-components
* to create an {@link BitmapLoad} capable of loading a model that represents either an image or a video.
*
* @param <T> The type of the model.
* @param <Y> The type of the resource that the image {@link ModelLoader} provides and the image {@link BitmapDecoder} can
* decode.`
* @param <Z> The type of resource that the video {@link ModelLoader} provides and the video {@link BitmapDecoder} can
* decode.
*/
public class ImageVideoBitmapLoadFactory<T, Y, Z> implements BitmapLoadFactory<T> {
private final ResourceBitmapLoadFactory<T, Y> imageLoadFactory;
private final ResourceBitmapLoadFactory<T, Z> videoLoadFactory;
private final Transformation transformation;
public ImageVideoBitmapLoadFactory(ResourceBitmapLoadFactory<T, Y> imageLoadFactory,
ResourceBitmapLoadFactory<T, Z> videoLoadFactory,
Transformation transformation) {
this.imageLoadFactory = imageLoadFactory;
this.videoLoadFactory = videoLoadFactory;
if (imageLoadFactory == null && videoLoadFactory == null) {
throw new IllegalArgumentException("You must provide at least a video model loader and a video decoder or"
+ " an image model loader and an image decoder");
}
if (transformation == null) {
throw new IllegalArgumentException("You must provide a non null transformation");
}
this.transformation = transformation;
}
@Override
public BitmapLoad getLoadTask(T model, int width, int height) {
BitmapLoad imageLoad = null;
if (imageLoadFactory != null) {
imageLoad = imageLoadFactory.getLoadTask(model, width, height);
}
BitmapLoad videoLoad = null;
if (videoLoadFactory != null) {
videoLoad = videoLoadFactory.getLoadTask(model, width, height);
}
return new ImageVideoBitmapLoad(imageLoad, videoLoad, width, height, transformation);
}
}
package com.bumptech.glide.loader.bitmap;
import com.bumptech.glide.loader.bitmap.model.ModelLoader;
import com.bumptech.glide.loader.bitmap.resource.ResourceFetcher;
import com.bumptech.glide.resize.load.BitmapLoad;
import com.bumptech.glide.resize.load.BitmapDecoder;
import com.bumptech.glide.resize.load.DecodeFormat;
import com.bumptech.glide.resize.load.ResourceBitmapLoad;
import com.bumptech.glide.resize.load.Transformation;
public class ResourceBitmapLoadFactory<T, Y> implements BitmapLoadFactory<T> {
private final ModelLoader<T, Y> modelLoader;
private final BitmapDecoder<Y> decoder;
private DecodeFormat decodeFormat;
public ResourceBitmapLoadFactory(ModelLoader<T, Y> modelLoader, BitmapDecoder<Y> decoder,
DecodeFormat decodeFormat) {
if (modelLoader == null) {
throw new IllegalArgumentException("Can't create load factory with null model loader");
}
if (decoder == null) {
throw new IllegalArgumentException("Can't create load factory with null decoder");
}
if (decodeFormat == null) {
throw new IllegalArgumentException("Can't create load factory with null decode format");
}
this.modelLoader = modelLoader;
this.decoder = decoder;
this.decodeFormat = decodeFormat;
}
@Override
public BitmapLoad getLoadTask(T model, int width, int height) {
final String id = modelLoader.getId(model);
final ResourceFetcher<Y> resourceFetcher = modelLoader.getResourceFetcher(model, width, height);
return new ResourceBitmapLoad<Y>(id, resourceFetcher, decoder, width, height, Transformation.NONE,
decodeFormat);
}
}
......@@ -4,6 +4,7 @@ import android.os.Handler;
import com.bumptech.glide.loader.bitmap.resource.ResourceFetcher;
import com.bumptech.glide.resize.cache.DiskCache;
import com.bumptech.glide.resize.cache.ResourceCache;
import com.bumptech.glide.resize.load.Transformation;
import java.io.InputStream;
import java.util.concurrent.ExecutorService;
......@@ -26,27 +27,16 @@ class DefaultResourceRunnerFactory implements ResourceRunnerFactory {
this.referenceCounter = referenceCounter;
}
/**
*
* @param id
* @param cacheDecoder
* @param fetcher
* @param decoder
* @param encoder
* @param metadata
* @param <T> The type of the data the resource will be decoded from.
* @param <Z> The type of the resource that will be decoded.
* @return
*/
@Override
public <T, Z> ResourceRunner<Z> build(String id, int width, int height,
ResourceDecoder<InputStream, Z> cacheDecoder, ResourceFetcher<T> fetcher, ResourceDecoder<T, Z> decoder,
ResourceEncoder<Z> encoder, Metadata metadata, EngineJobListener listener, ResourceCallback<Z> cb) {
Transformation<Z> transformation, ResourceEncoder<Z> encoder, Metadata metadata, EngineJobListener listener,
ResourceCallback<Z> cb) {
EngineJob<Z> engineJob = new EngineJob<Z>(id, resourceCache, mainHandler, referenceCounter, listener, cb);
SourceResourceRunner<T, Z> sourceRunner = new SourceResourceRunner<T, Z>(id, width, height, fetcher, decoder,
encoder, diskCache, metadata, engineJob);
transformation, encoder, diskCache, metadata, engineJob);
return new ResourceRunner<Z>(id, width, height, diskCache, cacheDecoder, sourceRunner, service, bgHandler,
engineJob);
......
......@@ -3,6 +3,7 @@ package com.bumptech.glide.resize;
import android.os.Build;
import com.bumptech.glide.loader.bitmap.resource.ResourceFetcher;
import com.bumptech.glide.resize.cache.ResourceCache;
import com.bumptech.glide.resize.load.Transformation;
import java.io.InputStream;
import java.util.HashMap;
......@@ -57,9 +58,9 @@ public class Engine implements EngineJobListener, ResourceCache.ResourceRemovedL
* @param <T> The type of data the resource will be decoded from.
* @param <Z> The type of the resource that will be decoded.
*/
public <T, Z> LoadStatus load(String id, int width, int height, ResourceDecoder<InputStream, Z> cacheDecoder,
ResourceFetcher<T> fetcher, ResourceDecoder<T, Z> decoder, ResourceEncoder<Z> encoder, Metadata metadata,
ResourceCallback<Z> cb) {
public <T, Z> LoadStatus load(String id, int width, int height, ResourceDecoder<InputStream, Z> cacheDecoder,
ResourceFetcher<T> fetcher, ResourceDecoder<T, Z> decoder, Transformation<Z> transformation,
ResourceEncoder<Z> encoder, Metadata metadata, ResourceCallback<Z> cb) {
Resource<Z> cached = cache.get(id);
if (cached != null) {
cb.onResourceReady(cached);
......@@ -73,8 +74,8 @@ public class Engine implements EngineJobListener, ResourceCache.ResourceRemovedL
return new LoadStatus(cb, job);
}
ResourceRunner<Z> runner = factory.build(id, width, height, cacheDecoder, fetcher, decoder, encoder, metadata,
this, cb);
ResourceRunner<Z> runner = factory.build(id, width, height, cacheDecoder, fetcher, decoder, transformation,
encoder, metadata, this, cb);
runners.put(id, runner);
runner.queue();
return new LoadStatus(cb, runner.getJob());
......
package com.bumptech.glide.resize;
import com.bumptech.glide.loader.bitmap.resource.ResourceFetcher;
import com.bumptech.glide.resize.load.Transformation;
import java.io.InputStream;
......@@ -11,6 +12,7 @@ interface ResourceRunnerFactory {
* @param cacheDecoder
* @param fetcher
* @param decoder
* @param transformation
* @param encoder
* @param metadata
* @param <T> The type of data the resource will be decoded from.
......@@ -19,5 +21,6 @@ interface ResourceRunnerFactory {
*/
public <T, Z> ResourceRunner<Z> build(String id, int width, int height,
ResourceDecoder<InputStream, Z> cacheDecoder, ResourceFetcher<T> fetcher, ResourceDecoder<T, Z> decoder,
ResourceEncoder<Z> encoder, Metadata metadata, EngineJobListener listener, ResourceCallback<Z> cb);
Transformation<Z> transformation, ResourceEncoder<Z> encoder, Metadata metadata,
EngineJobListener listener, ResourceCallback<Z> cb);
}
......@@ -2,6 +2,7 @@ package com.bumptech.glide.resize;
import com.bumptech.glide.loader.bitmap.resource.ResourceFetcher;
import com.bumptech.glide.resize.cache.DiskCache;
import com.bumptech.glide.resize.load.Transformation;
import java.io.OutputStream;
......@@ -16,6 +17,7 @@ public class SourceResourceRunner<T, Z> implements Runnable, DiskCache.Writer, P
private final int height;
private final ResourceFetcher<T> fetcher;
private final ResourceDecoder<T, Z> decoder;
private Transformation<Z> transformation;
private final ResourceEncoder<Z> encoder;
private DiskCache diskCache;
private Metadata metadata;
......@@ -23,13 +25,15 @@ public class SourceResourceRunner<T, Z> implements Runnable, DiskCache.Writer, P
private Resource<Z> result;
private volatile boolean isCancelled;
public SourceResourceRunner(String id, int width, int height, ResourceFetcher<T> resourceFetcher, ResourceDecoder<T, Z> decoder,
ResourceEncoder<Z> encoder, DiskCache diskCache, Metadata metadata, ResourceCallback<Z> cb) {
public SourceResourceRunner(String id, int width, int height, ResourceFetcher<T> resourceFetcher,
ResourceDecoder<T, Z> decoder, Transformation<Z> transformation, ResourceEncoder<Z> encoder,
DiskCache diskCache, Metadata metadata, ResourceCallback<Z> cb) {
this.id = id;
this.width = width;
this.height = height;
this.fetcher = resourceFetcher;
this.decoder = decoder;
this.transformation = transformation;
this.encoder = encoder;
this.diskCache = diskCache;
this.metadata = metadata;
......@@ -48,10 +52,16 @@ public class SourceResourceRunner<T, Z> implements Runnable, DiskCache.Writer, P
}
try {
result = null;
T toDecode = fetcher.loadResource(metadata);
if (toDecode != null) {
result = decoder.decode(toDecode, width, height);
Resource<Z> decoded = decoder.decode(toDecode, width, height);
if (decoded != null) {
Resource<Z> transformed = transformation.transform(decoded, width, height);
if (decoded != transformed) {
decoded.recycle();
}
result = transformed;
}
}
if (result != null) {
diskCache.put(id, this);
......
package com.bumptech.glide.resize.bitmap;
import android.graphics.Bitmap;
import com.bumptech.glide.resize.Resource;
import com.bumptech.glide.resize.bitmap_recycle.BitmapPool;
import com.bumptech.glide.resize.load.Transformation;
import com.bumptech.glide.resize.load.TransformationUtils;
/**
* Scale the image so that either the width of the image matches the given width and the height of the image is
* greater than the given height or vice versa, and then crop the larger dimension to match the given dimension.
*
* Does not maintain the image's aspect ratio
*/
public class CenterCrop implements Transformation<Bitmap> {
private BitmapPool pool;
public CenterCrop(BitmapPool pool) {
this.pool = pool;
}
@Override
public Resource<Bitmap> transform(Resource<Bitmap> resource, int outWidth, int outHeight) {
if (outWidth <= 0 || outHeight <= 0) {
throw new IllegalArgumentException("Cannot center crop image to width=" + outWidth + " and height="
+ outHeight);
}
final Bitmap toReuse = pool.get(outWidth, outHeight, resource.get().getConfig());
Bitmap transformed = TransformationUtils.centerCrop(toReuse, resource.get(), outWidth, outHeight);
if (toReuse != transformed && !pool.put(toReuse)) {
toReuse.recycle();
}
return new BitmapResource(transformed, pool);
}
@Override
public String getId() {
return "com.bumptech.glide.resize.load.Transformation.CenterCrop";
}
}
package com.bumptech.glide.resize.bitmap;
import android.graphics.Bitmap;
import com.bumptech.glide.resize.Resource;
import com.bumptech.glide.resize.bitmap_recycle.BitmapPool;
import com.bumptech.glide.resize.load.Transformation;
import com.bumptech.glide.resize.load.TransformationUtils;
/**
* Scale the image uniformly (maintaining the image's aspect ratio) so that one of the dimensions of the image
* will be equal to the given dimension and the other will be less than the given dimension
*/
public class FitCenter implements Transformation<Bitmap> {
private BitmapPool pool;
public FitCenter(BitmapPool pool) {
this.pool = pool;
}
@Override
public Resource<Bitmap> transform(Resource<Bitmap> resource, int outWidth, int outHeight) {
if (outWidth <= 0 || outHeight <= 0) {
throw new IllegalArgumentException("Cannot fit center image to within width=" + outWidth + " or height="
+ outHeight);
}
Bitmap transformed = TransformationUtils.fitCenter(resource.get(), pool, outWidth, outHeight);
return new BitmapResource(transformed, pool);
}
@Override
public String getId() {
return "com.bumptech.glide.resize.load.Transformation.FitCenter";
}
}
package com.bumptech.glide.resize.load;
import android.graphics.Bitmap;
import android.util.Log;
import com.bumptech.glide.loader.bitmap.resource.ResourceFetcher;
import com.bumptech.glide.resize.Metadata;
import com.bumptech.glide.resize.bitmap_recycle.BitmapPool;
/**
* A base {@link BitmapLoad} that composes {@link ResourceFetcher} and {@link BitmapDecoder} to decode a
* bitmap from either an image or a video.
*/
public class ImageVideoBitmapLoad implements BitmapLoad {
private static final String TAG = "IVBL";
private final String id;
private final int width;
private final int height;
private final BitmapLoad imageLoad;
private final BitmapLoad videoLoad;
private final Transformation transformation;
private Metadata metadata;
public ImageVideoBitmapLoad(BitmapLoad imageLoad, BitmapLoad videoLoad, int width,
int height, Transformation transformation) {
this.imageLoad = imageLoad;
this.videoLoad = videoLoad;
this.transformation = transformation;
this.width = width;
this.height = height;
StringBuilder idBuilder = new StringBuilder();
if (imageLoad != null) {
idBuilder.append(imageLoad.getId());
}
if (videoLoad != null) {
idBuilder.append(videoLoad.getId());
}
this.id = idBuilder.append(transformation.getId()).toString();
}
public void cancel() {
if (imageLoad != null) {
imageLoad.cancel();
}
if (videoLoad != null) {
videoLoad.cancel();
}
}
@Override
public Metadata getMetadata() {
return metadata;
}
@Override
public void setMetadata(Metadata metadata) {
this.metadata = metadata;
if (imageLoad != null) {
imageLoad.setMetadata(metadata);
}
if (videoLoad != null) {
videoLoad.setMetadata(metadata);
}
}
@Override
public Bitmap load(BitmapPool bitmapPool) throws Exception {
long now = System.currentTimeMillis();
Bitmap original = null;
if (imageLoad != null) {
original = imageLoad.load(bitmapPool);
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "loaded image in " + (System.currentTimeMillis() - now));
now = System.currentTimeMillis();
}
if (original == null && videoLoad != null) {
original = videoLoad.load(bitmapPool);
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "loaded video in " + (System.currentTimeMillis() - now));
now = System.currentTimeMillis();
}
if (original == null) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unable to decode either image or video");
}
return null;
}
Bitmap transformed = transformation.transform(original, bitmapPool, width, height);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "transformed in " + (System.currentTimeMillis() - now));
}
if (original != transformed) {
bitmapPool.put(original);
}
return transformed;
}
public String getId() {
return id;
}
}
package com.bumptech.glide.resize.load;
import android.graphics.Bitmap;
import com.bumptech.glide.resize.bitmap_recycle.BitmapPool;
import com.bumptech.glide.resize.Resource;
import java.util.List;
/**
* A transformation that applies an ordered array of one or more transformations to an image.
*/
public class MultiTransformation extends Transformation {
private Transformation[] transformations;
private List<Transformation> transformationList;
public class MultiTransformation<T> implements Transformation<T> {
private Transformation<T>[] transformations;
private List<Transformation<T>> transformationList;
public MultiTransformation(Transformation... transformations) {
public MultiTransformation(Transformation<T>... transformations) {
if (transformations.length < 1) {
throw new IllegalArgumentException("MultiTransformation must contain at least one Transformation");
}
this.transformations = transformations;
}
public MultiTransformation(List<Transformation> transformationList) {
public MultiTransformation(List<Transformation<T>> transformationList) {
if (transformationList.size() < 1) {
throw new IllegalArgumentException("MultiTransformation must contain at least one Transformation");
}
......@@ -28,28 +27,28 @@ public class MultiTransformation extends Transformation {
}
@Override
public Bitmap transform(Bitmap bitmap, BitmapPool pool, int outWidth, int outHeight) {
public Resource<T> transform(Resource<T> resource, int outWidth, int outHeight) {
// Set current to null so we don't recycle our original bitmap. Instead rely on the caller of this method to do
// so.
Bitmap current = null;
Resource<T> current = null;
if (transformations != null) {
for (Transformation transformation : transformations) {
current = transform(current, transformation, pool, outWidth, outHeight);
for (Transformation<T> transformation : transformations) {
current = transform(current, transformation, outWidth, outHeight);
}
} else {
for (Transformation transformation : transformationList) {
current = transform(current, transformation, pool, outWidth, outHeight);
for (Transformation<T> transformation : transformationList) {
current = transform(current, transformation, outWidth, outHeight);
}
}
return current;
}
private Bitmap transform(Bitmap current, Transformation transformation, BitmapPool pool, int outWidth,
private Resource<T> transform(Resource<T> current, Transformation<T> transformation, int outWidth,
int outHeight) {
Bitmap transformed = transformation.transform(current, pool, outWidth, outHeight);
if (current != null && current != transformed && !pool.put(current)) {
Resource<T> transformed = transformation.transform(current, outWidth, outHeight);
if (current != null && current != transformed) {
current.recycle();
}
......
package com.bumptech.glide.resize.load;
import android.graphics.Bitmap;
import com.bumptech.glide.loader.bitmap.resource.ResourceFetcher;
import com.bumptech.glide.resize.Metadata;
import com.bumptech.glide.resize.bitmap_recycle.BitmapPool;
public class ResourceBitmapLoad<T> implements BitmapLoad {
private final String id;
private final ResourceFetcher<T> fetcher;
private final BitmapDecoder<T> decoder;
private final int width;
private final int height;
private final Transformation transformation;
private final DecodeFormat decodeFormat;
private Metadata metadata;
public ResourceBitmapLoad(String modelId, ResourceFetcher<T> fetcher, BitmapDecoder<T> decoder, int width,
int height, Transformation transformation, DecodeFormat decodeFormat) {
this.fetcher = fetcher;
this.decoder = decoder;
this.width = width;
this.height = height;
this.transformation = transformation;
this.decodeFormat = decodeFormat;
this.id = modelId + decoder.getId() + width + height;
}
@Override
public String getId() {
return id;
}
@Override
public void cancel() {
if (fetcher != null) {
fetcher.cancel();
}
}
@Override
public Metadata getMetadata() {
return metadata;
}
@Override
public void setMetadata(Metadata metadata) {
this.metadata = metadata;
}
@Override
public Bitmap load(BitmapPool bitmapPool) throws Exception {
Bitmap original = null;
if (fetcher == null || decoder == null) {
return original;
}
T resource = fetcher.loadResource(metadata);
if (resource != null) {
original = decoder.decode(resource, bitmapPool, width, height, decodeFormat);
}
Bitmap transformed = original;
if (original != null) {
transformed = transformation.transform(original, bitmapPool, width, height);
if (transformed != null && transformed != original) {
bitmapPool.put(original);
}
}
return transformed;
}
}
package com.bumptech.glide.resize.load;
import android.graphics.Bitmap;
import com.bumptech.glide.resize.Resource;
import com.bumptech.glide.resize.bitmap.BitmapResource;
import com.bumptech.glide.resize.bitmap_recycle.BitmapPool;
/**
* A class for performing an arbitrary transformation on a bitmap
* @param <T> The type of the resource being transformed.
*/
public abstract class Transformation {
private final String id = getClass().toString();
public interface Transformation<T> {
/**
* Scale the image so that either the width of the image matches the given width and the height of the image is
* greater than the given height or vice versa, and then crop the larger dimension to match the given dimension.
*
* Does not maintain the image's aspect ratio
* A noop Transformation that simply returns the given bitmap
*/
public static Transformation CENTER_CROP = new Transformation() {
public static Transformation NONE = new Transformation() {
@Override
public Bitmap transform(Bitmap bitmap, BitmapPool pool, int outWidth, int outHeight) {
if (outWidth <= 0 || outHeight <= 0) {
throw new IllegalArgumentException("Cannot center crop image to width=" + outWidth + " and height="
+ outHeight);
}
final Bitmap toReuse = pool.get(outWidth, outHeight, bitmap.getConfig());
return TransformationUtils.centerCrop(toReuse, bitmap, outWidth, outHeight);
public Resource transform(Resource resource, int outWidth, int outHeight) {
return resource;
}
};
/**
* Scale the image uniformly (maintaining the image's aspect ratio) so that one of the dimensions of the image
* will be equal to the given dimension and the other will be less than the given dimension
*/
public static Transformation FIT_CENTER = new Transformation() {
@Override
public Bitmap transform(Bitmap bitmap, BitmapPool pool, int outWidth, int outHeight) {
if (outWidth <= 0 || outHeight <= 0) {
throw new IllegalArgumentException("Cannot fit center image to within width=" + outWidth + " or height="
+ outHeight);
}
return TransformationUtils.fitCenter(bitmap, pool, outWidth, outHeight);
public String getId() {
return "com.bumptech.glide.resize.load.Transformation.NONE";
}
};
/**
* A noop Transformation that simply returns the given bitmap
*/
public static Transformation NONE = new Transformation() {
@Override
public Bitmap transform(Bitmap bitmap, BitmapPool pool, int outWidth, int outHeight) {
return bitmap;
}
};
/**
* Transform the given bitmap. It is also acceptable to simply return the given bitmap if no transformation is
* required.
*
* @param bitmap The bitmap to transform
* @param pool A bitmap pool to obtain reused bitmaps from and release unneeded bitmaps to. It is always safe
* to attempt to retrieve bitmaps. However, any bitmaps released to the pool must not be referenced
* elsewhere or returned.
* @param resource The resource to transform
* @param outWidth The width of the view or target the bitmap will be displayed in
* @param outHeight The height of the view or target the bitmap will be displayed in
* @return The transformed bitmap
*/
public abstract Bitmap transform(Bitmap bitmap, BitmapPool pool, int outWidth, int outHeight);
public abstract Resource<T> transform(Resource<T> resource, int outWidth, int outHeight);
/**
* A method to get a unique identifier for this particular transformation that can be used as part of a cache key
* A method to get a unique identifier for this particular transformation that can be used as part of a cache key.
* The fully qualified class name for this class is appropriate if written out, but getClass().getName() is not
* because the name may be changed by proguard.
*
* @return A string that uniquely identifies this transformation from other transformations
*/
public String getId() {
return id;
}
public String getId();
}
......@@ -17,7 +17,9 @@ import com.bumptech.glide.resize.Resource;
import com.bumptech.glide.resize.ResourceCallback;
import com.bumptech.glide.resize.ResourceDecoder;
import com.bumptech.glide.resize.ResourceEncoder;
import com.bumptech.glide.resize.bitmap.BitmapResource;
import com.bumptech.glide.resize.load.DecodeFormat;
import com.bumptech.glide.resize.load.Transformation;
import com.bumptech.glide.resize.target.Target;
import java.io.InputStream;
......@@ -35,6 +37,7 @@ public class BitmapRequest<T, Z> implements Request, Target.SizeReadyCallback, R
private final int errorResourceId;
private final Context context;
private final Class<Z> resourceClass;
private final Transformation<Bitmap> transformation;
private DecodeFormat decodeFormat;
private final int animationId;
private final RequestCoordinator requestCoordinator;
......@@ -72,6 +75,7 @@ public class BitmapRequest<T, Z> implements Request, Target.SizeReadyCallback, R
this.decodeFormat = builder.decodeFormat;
this.engine = builder.engine;
this.requestContext = builder.requestContext;
this.transformation = builder.transformation;
}
@Override
......@@ -155,7 +159,8 @@ public class BitmapRequest<T, Z> implements Request, Target.SizeReadyCallback, R
final Metadata metadata = new Metadata(priority, decodeFormat);
loadedFromMemoryCache = true;
loadStatus = engine.load(id, width, height, cacheDecoder, resourceFetcher, decoder, encoder, metadata, this);
loadStatus = engine.load(id, width, height, cacheDecoder, resourceFetcher, decoder, transformation,
encoder, metadata, this);
loadedFromMemoryCache = resource != null;
}
......
......@@ -5,18 +5,18 @@ import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.view.animation.Animation;
import com.bumptech.glide.RequestListener;
import com.bumptech.glide.loader.bitmap.BitmapLoadFactory;
import com.bumptech.glide.resize.Engine;
import com.bumptech.glide.resize.ImageManager;
import com.bumptech.glide.resize.Priority;
import com.bumptech.glide.resize.RequestContext;
import com.bumptech.glide.resize.load.DecodeFormat;
import com.bumptech.glide.resize.load.Transformation;
import com.bumptech.glide.resize.target.Target;
/**
* A simple builder class for {@link BitmapRequest}.
*
* @param <T, Z> The model type the {@link BitmapRequest} will load an {@link Bitmap} from.
* @param <T> The model type the {@link BitmapRequest} will load an {@link Bitmap} from.
* @param <Z> The resource type the {@link BitmapRequest} will load an {@link Bitmap} from.
*/
public class BitmapRequestBuilder<T, Z> {
......@@ -38,6 +38,7 @@ public class BitmapRequestBuilder<T, Z> {
DecodeFormat decodeFormat = DecodeFormat.PREFER_RGB_565;
Engine engine;
RequestContext requestContext;
Transformation<Bitmap> transformation;
public BitmapRequestBuilder(Class<Z> resourceClass) {
this.resourceClass = resourceClass;
......@@ -128,6 +129,11 @@ public class BitmapRequestBuilder<T, Z> {
return this;
}
public BitmapRequestBuilder<T, Z> setTransformation(Transformation<Bitmap> transformation) {
this.transformation = transformation;
return this;
}
public BitmapRequest<T, Z> build() {
return new BitmapRequest<T, Z>(this);
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册