Codifica una App Android de Galería de Imágenes con Glide
Spanish (Español) translation by Rafael Chavarría (you can also view the original English article)

1. ¿Qué es Glide?
Glide es una popular librería Android de código abierto para cargar imágenes, videos y GIFs animados. Con Glide puedes cargar y mostrar medios de muchas fuentes diferentes, tales como servidores remotos o el sistema local de archivos.
Por defecto, Glide usa una implementación personalizada de HttpURLConnection para cargar imágenes en internet. Sin embargo, Glide también proporciona complementos para otras librerías populares de red como Volley o OkHttp.
2. ¿Así Que Por Qué Usar Glide?
Desarrollar tu propia funcionalidad de carga de medios y mostrar en Java puede ser un gran dolor: tienes que encargarte de cachear, decodificar, administrar conexiones de red, hilación, manejo de excepciones y más. Glide es una librería fácil de usar, bien planeada, bien documentada y completamente probada que puede ahorrarte mucho tiempo---y ahorrarte algunos dolores de cabeza.
En este tutorial, aprenderemos sobre Glide 3 construyendo una simple app de galería de imágenes. Esta cargará las imágenes vía el internet y las mostrará como viñetas en un RecyclerView y cuando un usuario de clic en una imagen, abrirá una actividad de detalle conteniendo la imagen más grande.
3. Crea un Proyecto Android Studio
Inicia tu Android Studio y crea un nuevo proyecto con una actividad vacía llamada MainActivity.
2. Declara Dependencias
Después de crear un nuevo proyecto, especifica las siguientes dependencias en tu build.gradle.
1 |
repositories { |
2 |
mavenCentral() // jcenter() works as well because it pulls from Maven Central |
3 |
}
|
4 |
|
5 |
dependencies { |
6 |
// Glide
|
7 |
compile 'com.github.bumptech.glide:glide:3.7.0' |
8 |
|
9 |
// Recyclerview
|
10 |
compile 'com.android.support:recyclerview-v7:25.1.1' |
11 |
}
|
12 |
O con Maven:
1 |
<dependency>
|
2 |
<groupId>com.github.bumptech.glide</groupId> |
3 |
<artifactId>glide</artifactId> |
4 |
<version>3.7.0</version> |
5 |
</dependency>
|
6 |
<dependency>
|
7 |
<groupId>com.google.android</groupId> |
8 |
<artifactId>support-v4</artifactId> |
9 |
<version>r7</version> |
10 |
</dependency>
|
Asegúrate de que sincronizas tu proyecto después de agregar la dependencia Glide.
Librerías de Integración
Si quieres usar una librería de trabajo en red como OkHttp o Volley en tu proyecto para operaciones de red, se recomienda que incluyas la integración Glide específica para la librería que estás usando (en vez del de por defecto que engloba HttpURLConnection).
Volley
1 |
dependencies { |
2 |
|
3 |
compile 'com.github.bumptech.glide:glide:3.7.0' |
4 |
compile 'com.github.bumptech.glide:volley-integration:1.4.0@aar' |
5 |
compile 'com.mcxiaoke.volley:library:1.0.8' |
6 |
}
|
OkHttp
1 |
dependencies { |
2 |
|
3 |
// okhttp 3
|
4 |
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar' |
5 |
compile 'com.squareup.okhttp3:okhttp:3.2.0' |
6 |
|
7 |
// okhttp 2
|
8 |
compile 'com.github.bumptech.glide:okhttp-integration:1.4.0@aar' |
9 |
compile 'com.squareup.okhttp:okhttp:2.2.0' |
10 |
}
|
Puedes visitar la guía oficial de integración de librerías Glide para más información.
3. Agrega Permiso de Internet
Ya que Glide va a realizar una petición de red para cargar imágenes vía internet, necesitamos incluir el permiso INTERNET en nuestro AndroidManifest.xml.
1 |
<uses-permission android:name="android.permission.INTERNET" /> |
4. Crea el Diseño
Comenzaremos creando nuestro RecyclerView.
1 |
<?xml version="1.0" encoding="utf-8"?>
|
2 |
<RelativeLayout
|
3 |
xmlns:android="http://schemas.android.com/apk/res/android" |
4 |
xmlns:tools="http://schemas.android.com/tools" |
5 |
android:id="@+id/activity_main" |
6 |
android:layout_width="match_parent" |
7 |
android:layout_height="match_parent"> |
8 |
|
9 |
<android.support.v7.widget.RecyclerView
|
10 |
android:id="@+id/rv_images" |
11 |
android:layout_width="match_parent" |
12 |
android:layout_height="match_parent"/> |
13 |
</RelativeLayout>
|
Creando el Diseño de Elemento Personalizado
Después, creamos el diseño XML que será usado para cada elemento (ImageView) dentro del RecyclerView.
1 |
<?xml version="1.0" encoding="utf-8"?>
|
2 |
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
3 |
android:orientation="vertical" |
4 |
android:layout_width="match_parent" |
5 |
android:layout_height="wrap_content"> |
6 |
<ImageView
|
7 |
android:id="@+id/iv_photo" |
8 |
android:adjustViewBounds="true" |
9 |
android:layout_height="200dp" |
10 |
android:scaleType="centerCrop" |
11 |
android:layout_margin="2dp" |
12 |
android:layout_width="match_parent"/> |
13 |
|
14 |
</LinearLayout>
|
Ahora que hemos creado el diseño, el siguiente paso es crear el adaptador RecyclerView para poblar información. Antes de que hagamos eso, sin embargo, creemos nuestro modelo simple de datos.
5. Crea un Modelo de Datos
Vamos a definir un simple modelo de datos para nuestro RecyclerView. Este modelo implementa Parcelable para mayor desempeño de transporte de datos desde un componente a otros. En nuestro caso, la información será transportada de SpaceGalleryActivity a SpacePhotoActivity.
1 |
import android.os.Parcel; |
2 |
import android.os.Parcelable; |
3 |
|
4 |
public class SpacePhoto implements Parcelable { |
5 |
|
6 |
private String mUrl; |
7 |
private String mTitle; |
8 |
|
9 |
public SpacePhoto(String url, String title) { |
10 |
mUrl = url; |
11 |
mTitle = title; |
12 |
}
|
13 |
|
14 |
protected SpacePhoto(Parcel in) { |
15 |
mUrl = in.readString(); |
16 |
mTitle = in.readString(); |
17 |
}
|
18 |
|
19 |
public static final Creator<SpacePhoto> CREATOR = new Creator<SpacePhoto>() { |
20 |
@Override
|
21 |
public SpacePhoto createFromParcel(Parcel in) { |
22 |
return new SpacePhoto(in); |
23 |
}
|
24 |
|
25 |
@Override
|
26 |
public SpacePhoto[] newArray(int size) { |
27 |
return new SpacePhoto[size]; |
28 |
}
|
29 |
};
|
30 |
|
31 |
public String getUrl() { |
32 |
return mUrl; |
33 |
}
|
34 |
|
35 |
public void setUrl(String url) { |
36 |
mUrl = url; |
37 |
}
|
38 |
|
39 |
public String getTitle() { |
40 |
return mTitle; |
41 |
}
|
42 |
|
43 |
public void setTitle(String title) { |
44 |
mTitle = title; |
45 |
}
|
46 |
|
47 |
public static SpacePhoto[] getSpacePhotos() { |
48 |
|
49 |
return new SpacePhoto[]{ |
50 |
new SpacePhoto("http://i.imgur.com/zuG2bGQ.jpg", "Galaxy"), |
51 |
new SpacePhoto("http://i.imgur.com/ovr0NAF.jpg", "Space Shuttle"), |
52 |
new SpacePhoto("http://i.imgur.com/n6RfJX2.jpg", "Galaxy Orion"), |
53 |
new SpacePhoto("http://i.imgur.com/qpr5LR2.jpg", "Earth"), |
54 |
new SpacePhoto("http://i.imgur.com/pSHXfu5.jpg", "Astronaut"), |
55 |
new SpacePhoto("http://i.imgur.com/3wQcZeY.jpg", "Satellite"), |
56 |
};
|
57 |
}
|
58 |
|
59 |
@Override
|
60 |
public int describeContents() { |
61 |
return 0; |
62 |
}
|
63 |
|
64 |
@Override
|
65 |
public void writeToParcel(Parcel parcel, int i) { |
66 |
parcel.writeString(mUrl); |
67 |
parcel.writeString(mTitle); |
68 |
}
|
69 |
}
|
6. Crea el Adaptador
Crearemos un adaptador para poblar el RecyclerView con datos. También implementaremos un listener de clic para abrir la actividad de detalle---SpacePhotoActivity---pasándole una instancia de SpacePhoto como un extra. La actividad de detalle mostrará un acercamiento de la imagen. La crearemos en una sección posterior.
1 |
public class MainActivity extends AppCompatActivity { |
2 |
// ...
|
3 |
private class ImageGalleryAdapter extends RecyclerView.Adapter<ImageGalleryAdapter.MyViewHolder> { |
4 |
|
5 |
@Override
|
6 |
public ImageGalleryAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |
7 |
|
8 |
Context context = parent.getContext(); |
9 |
LayoutInflater inflater = LayoutInflater.from(context); |
10 |
View photoView = inflater.inflate(R.layout.item_photo, parent, false); |
11 |
ImageGalleryAdapter.MyViewHolder viewHolder = new ImageGalleryAdapter.MyViewHolder(photoView); |
12 |
return viewHolder; |
13 |
}
|
14 |
|
15 |
@Override
|
16 |
public void onBindViewHolder(ImageGalleryAdapter.MyViewHolder holder, int position) { |
17 |
|
18 |
SpacePhoto spacePhoto = mSpacePhotos[position]; |
19 |
ImageView imageView = holder.mPhotoImageView; |
20 |
}
|
21 |
|
22 |
@Override
|
23 |
public int getItemCount() { |
24 |
return (mSpacePhotos.length); |
25 |
}
|
26 |
|
27 |
public class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { |
28 |
|
29 |
public ImageView mPhotoImageView; |
30 |
|
31 |
public MyViewHolder(View itemView) { |
32 |
|
33 |
super(itemView); |
34 |
mPhotoImageView = (ImageView) itemView.findViewById(R.id.iv_photo); |
35 |
itemView.setOnClickListener(this); |
36 |
}
|
37 |
|
38 |
@Override
|
39 |
public void onClick(View view) { |
40 |
|
41 |
int position = getAdapterPosition(); |
42 |
if(position != RecyclerView.NO_POSITION) { |
43 |
SpacePhoto spacePhoto = mSpacePhotos[position]; |
44 |
Intent intent = new Intent(mContext, SpacePhotoActivity.class); |
45 |
intent.putExtra(SpacePhotoActivity.EXTRA_SPACE_PHOTO, spacePhoto); |
46 |
startActivity(intent); |
47 |
}
|
48 |
}
|
49 |
}
|
50 |
|
51 |
private SpacePhoto[] mSpacePhotos; |
52 |
private Context mContext; |
53 |
|
54 |
public ImageGalleryAdapter(Context context, SpacePhoto[] spacePhotos) { |
55 |
mContext = context; |
56 |
mSpacePhotos = spacePhotos; |
57 |
}
|
58 |
}
|
59 |
}
|
7. Cargando Imágenes Desde una URL
Aquí es donde necesitamos que Glide haga su trabajo--para traer imágenes de internet y mostrarlas en los ImageViews individuales, usando nuestros método onBindViewHolder() RecyclerView mientras el usuario desliza la app.
1 |
// ...
|
2 |
@Override
|
3 |
public void onBindViewHolder(MyViewHolder holder, int position) { |
4 |
Photo photo = mPhotoList.get(position); |
5 |
ImageView imageView = holder.mPhotoImageView; |
6 |
|
7 |
Glide.with(mContext) |
8 |
.load(spacePhoto.getUrl()) |
9 |
.placeholder(R.drawable.ic_cloud_off_red) |
10 |
.into(imageView); |
11 |
}
|
12 |
// ...
|
Paso a paso, aquí está lo que las llamadas a Glide están haciendo:
-
with(Context context): comenzamos el proceso de carga primero pasando nuestro contexto en el métodowith(). -
load(String string): la fuente de la imagen es especificada como una ruta de directorio, una URI o una URL. -
placeholder(int resourceId): una id de recurso de aplicación local, preferentemente un drawable, que será un contenedor hasta que la imagen sea cargada y mostrada.
-
into(ImageView imageView): la vista de imagen objetivo en la cuál la imagen será colocada.
Ten en cuenta que Glide puede cargar imágenes locales también, solo pasa el id de recurso de Android, la ruta de archivo o una URI como un argumento al método load().
Redimensión de Imagen y Transformación
Puedes re-dimensionar la imagen antes de que sea mostrada en el ImageView con el método .override(int width, int height) de Glide. Esto es útil para crear viñetas en tu app cuando se carga un tamaño de imagen diferente desde el servidor. Nota que las dimensiones están en pixeles no dp.
La siguiente transformación de imagen también está disponible:
-
fitCenter(): escala la imagen uniformemente (manteniendo la relación de aspecto de la imagen) de manera que la imagen encajará en el área dada. La imagen entera será visible, pero podría haber padding vertical u horizontal. -
centerCrop():escala la imagen uniformemente (manteniendo la relación de aspecto de la imagen) de manera que la imagen llena el área dada, mostrando tanto de la imagen como sea posible. Si es necesario, la imagen será cortada horizontalmente o verticalmente para encajar.
8. Inicializando el Adaptador
Aquí creamos nuestro RecyclerView con GridLayoutManager como el administrador de diseño, inicializamos nuestro adaptador, y lo enlazamos al RecyclerView.
1 |
// ...
|
2 |
@Override
|
3 |
protected void onCreate(Bundle savedInstanceState) { |
4 |
super.onCreate(savedInstanceState); |
5 |
setContentView(R.layout.activity_main); |
6 |
|
7 |
RecyclerView.LayoutManager layoutManager = new GridLayoutManager(this, 2); |
8 |
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rv_images); |
9 |
recyclerView.setHasFixedSize(true); |
10 |
recyclerView.setLayoutManager(layoutManager); |
11 |
|
12 |
ImageGalleryAdapter adapter = new ImageGalleryAdapter(this, SpacePhoto.getSpacePhotos()); |
13 |
recyclerView.setAdapter(adapter); |
14 |
|
15 |
}
|
16 |
// ...
|
9. Creando la Actividad Detalle
Crea una nueva actividad y nómbrala SpacePhotoActivity. Obtenemos el extra SpacePhoto y carga la imagen con Glide como hicimos antes. Aquí estamos esperando que el archivo o URL sea un Bitmap, así que usaremos asBitmap() para hacer que Glide reciba un Bitmap. De otro modo la carga fallará y el callback .error() será disparado---causando que el recurso drawable devuelto desde el callback de error sea mostrado. Podrías usar asGif() si quisieras asegurar que tu imagen cargada sea un GIF. (explicaré cómo trabajan los GIFs en Glide en breve.)
1 |
import android.graphics.Bitmap; |
2 |
import android.graphics.Color; |
3 |
import android.os.Bundle; |
4 |
import android.support.v7.app.AppCompatActivity; |
5 |
import android.support.v7.graphics.Palette; |
6 |
import android.view.ViewGroup; |
7 |
import android.widget.ImageView; |
8 |
import com.bumptech.glide.Glide; |
9 |
import com.bumptech.glide.request.RequestListener; |
10 |
import com.bumptech.glide.request.target.Target; |
11 |
|
12 |
public class SpacePhotoActivity extends AppCompatActivity { |
13 |
|
14 |
public static final String EXTRA_SPACE_PHOTO = "SpacePhotoActivity.SPACE_PHOTO"; |
15 |
private ImageView mImageView; |
16 |
|
17 |
@Override
|
18 |
protected void onCreate(Bundle savedInstanceState) { |
19 |
super.onCreate(savedInstanceState); |
20 |
setContentView(R.layout.activity_photo_detail); |
21 |
|
22 |
mImageView = (ImageView) findViewById(R.id.image); |
23 |
SpacePhoto spacePhoto = getIntent().getParcelableExtra(EXTRA_SPACE_PHOTO); |
24 |
|
25 |
Glide.with(this) |
26 |
.load(spacePhoto.getUrl()) |
27 |
.asBitmap() |
28 |
.error(R.drawable.ic_cloud_off_red) |
29 |
.diskCacheStrategy(DiskCacheStrategy.SOURCE) |
30 |
.into(mImageView); |
31 |
}
|
32 |
}
|
Nota que también inicializamos un caché único para las imágenes cargadas: DiskCacheStrategy.SOURCE. Explicaré más sobre cacheo en una sección posterior.
El Diseño de Detalle
Aquí hay un diseño para mostrar la actividad de detalle. Solo muestra un ImageView deslizable que mostrará la versión de resolución completa de la imagen cargada.
1 |
<?xml version="1.0" encoding="utf-8"?>
|
2 |
<LinearLayout
|
3 |
xmlns:android="http://schemas.android.com/apk/res/android" |
4 |
android:layout_width="match_parent" |
5 |
android:layout_height="match_parent" |
6 |
android:orientation="vertical"> |
7 |
|
8 |
<ScrollView
|
9 |
android:layout_width="match_parent" |
10 |
android:layout_height="match_parent"> |
11 |
|
12 |
<LinearLayout
|
13 |
android:id="@+id/activity_character" |
14 |
android:layout_width="match_parent" |
15 |
android:layout_height="wrap_content" |
16 |
android:orientation="vertical" |
17 |
android:layout_gravity="center_vertical"> |
18 |
|
19 |
<ImageView
|
20 |
android:id="@+id/image" |
21 |
android:layout_width="match_parent" |
22 |
android:layout_height="wrap_content" |
23 |
android:adjustViewBounds="true" |
24 |
android:scaleType="fitCenter"/> |
25 |
|
26 |
</LinearLayout>
|
27 |
</ScrollView>
|
28 |
</LinearLayout>
|
10. Cacheando en Glide
Si miras de cerca, verás que cuando revisitas una imagen que fue previamente cargada, esta carga incluso más rápido que antes. ¿Qué la hizo más rápida? El sistema de cacheo de Glide, eso es.
Después de que una imagen ha sido cargada una vez desde internet, Glide la cacheará en memoria y en el disco, ahorrando peticiones de red repetidas y permitiendo entrega más rápida de la imagen. Así que, Glide primero revisará si una imagen está disponible ya sea en memoria o en el disco antes de cargarla desde la red.
Dependiendo de tu aplicación, sin embargo, podrías querer evitar el cacheo--por ejemplo si las imágenes que están siendo mostradas cambiarán regularmente y no serán recargadas.
¿Así Que Cómo Deshabilitas el Cacheo?
Puedes evitar el cacheo de memoria llamando .skipMemoryCache(true). Pero ten en cuenta que la imagen aún será cacheada en el disco--para prevenir eso también, usas el método .diskCacheStrategy(DiskCacheStrategy strategy), que toma uno de los siguientes valores enum:
-
DiskCacheStrategy.NONE: no se guarda información al caché. -
DiaskCacheStrategy.SOURCE: información original es guardada en caché. -
DiskCacheStrategy.RESULT: guarda el resultado de la información después de las transformaciones al caché. -
DiskCacheStrategy.ALL: los cachea información original y datos transformados.
Para evitar cacheo de memoria y disco juntos, solo llama ambos métodos uno después del otro:
1 |
Glide.with(this) |
2 |
.load(spacePhoto.getUrl()) |
3 |
.asBitmap() |
4 |
.skipMemoryCache(true) |
5 |
.diskCacheStrategy(DiskCacheStrategy.NONE) |
6 |
.into(imageView); |
11. Listeners de Petición
En Glide, puedes implementar un RequestListener para monitorear el estado de la petición que hiciste mientras la imagen carga. Solo uno de estos métodos será llamado.
-
onException(): disparado siempre que una excepción ocurra para que puedas manejar excepciones en este método. -
onResourceReady(): disparado cuando la imagen es cargada exitosamente.
Regresando a nuestra app de galería de imagen, modifiquemos un poco la vista usando un objeto RequestListener que establecerá el bitmap al ImageView y también cambia el color de fondo del diseño extrayendo el color oscuro y vibrante de nuestra imagen usando la API Android Palette.
1 |
// ...
|
2 |
@Override
|
3 |
protected void onCreate(Bundle savedInstanceState) { |
4 |
// ...
|
5 |
Glide.with(this) |
6 |
.load(spacePhoto.getUrl()) |
7 |
.asBitmap() |
8 |
.error(R.drawable.ic_cloud_off_red) |
9 |
.listener(new RequestListener<String, Bitmap>() { |
10 |
|
11 |
@Override
|
12 |
public boolean onException(Exception e, String model, Target<Bitmap> target, boolean isFirstResource) { |
13 |
return false; |
14 |
}
|
15 |
|
16 |
@Override
|
17 |
public boolean onResourceReady(Bitmap resource, String model, Target<Bitmap> target, boolean isFromMemoryCache, boolean isFirstResource) { |
18 |
|
19 |
onPalette(Palette.from(resource).generate()); |
20 |
mImageView.setImageBitmap(resource); |
21 |
|
22 |
return false; |
23 |
}
|
24 |
|
25 |
public void onPalette(Palette palette) { |
26 |
if (null != palette) { |
27 |
ViewGroup parent = (ViewGroup) mImageView.getParent().getParent(); |
28 |
parent.setBackgroundColor(palette.getDarkVibrantColor(Color.GRAY)); |
29 |
}
|
30 |
}
|
31 |
})
|
32 |
.diskCacheStrategy(DiskCacheStrategy.SOURCE) |
33 |
.into(mImageView); |
34 |
}
|
35 |
// ...
|
Aquí podrías también esconder un diálogo de progreso si tuvieras uno. Con este último cambio, asegúrate de incluir la dependencia Palette en tu build.gradle:
1 |
dependencies { |
2 |
// ...
|
3 |
compile 'com.android.support:palette-v7:25.1.1' |
4 |
}
|
12. Probando la App
Finalmente, ¡puedes ejecutar la app! Da clic en una viñeta para obtener una versión de tamaño completo de la imagen.

10. Animaciones
Si ejecutas la app, notarás una animación de fundido que sucede mientras la imagen está siendo mostrada. Glide tiene esto habilitado por defecto, pero puedes deshabilitarlo llamando dontAnimate(), que causará que la imagen se muestre sin ninguna animación. También puedes personalizar la animación de fundido llamando crossFade(int duration), pasando la duración en milisegundos ya sea para acelerarlo o alentarlo---300 milisegundos es el ajuste por defecto.
GIFs Animados
Es muy simple mostrar un GIF animado en tu app usando Glide. Funciona igual que mostrar una imagen ordinaria.
1 |
ImageView gifImageView = (ImageView) findViewById(R.id.iv_gif); |
2 |
|
3 |
Glide.with(this) |
4 |
.load("http://i.imgur.com/Vth6CBz.gif") |
5 |
.asGif() |
6 |
.placeholder(R.drawable.ic_cloud_off_red) |
7 |
.error(R.drawable.ic_cloud_off_red) |
8 |
.into(gifImageView); |
Si estás esperando que la imagen sea un GIF, llama asGif()---esto asegura que Glide recibe un GIF, de otro modo la carga fallará y el Drawable pasado al método .error() será mostrado en su lugar.
Reproduciendo Video
Desafortunadamente, Glide no soporta carga y despliegue de video vía URL. En su lugar solo puede cargar y mostrar videos ya almacenados en el teléfono. Muestra un video pasando su URI al método load().
1 |
Glide.with(context) |
2 |
.load(Uri.fromFile(new File("your/video/file/path")) |
3 |
.into(imageView) |
Conclusión
¡Buen trabajo! En este tutorial, construiste una app de galería de imagen completa con Glide, y a lo largo del camino aprendiste como trabaja la librería y cómo puedes integrarla en tu propio proyecto. También has aprendido cómo mostrar imágenes locales y remotas, cómo mostrar GIFs animados y videos, cómo aplicar transformaciones de imagen como re-dimensión. No solo eso, sino que has visto lo fácil que es habilitar el cacheo, manejo de errores y listeners personalizados de petición.
Para aprender más sobre Glide, puedes referirte a su documentación oficial. Para aprender más sobre codificar para Android, ¡revisa algunos de nuestros otros cursos y tutoriales aquí en Envato Tuts+!
Android SDKCómo Hacer Llamadas y Usar SMS en Aplicaciones AndroidChike Mgbemena
SDK de AndroidAndroid Things: Tu Primer ProyectoPaul Trebilcox-Ruiz
SDK de AndroidCrea un Visor de Video Android Cardboard 360Paul Trebilcox-Ruiz
SDK de AndroidToma Fotografías Con Tu Aplicación AndroidAshraff Hathibelagal



