Picasso 是一个流行的开源 android 库,用于加载本地和远程图像。了解如何轻松使用它来处理您的图像加载需求。
1. 毕加索Picasso是什么?
Picasso (名字的灵感来源于著名的法国艺术家 Pablo Picasso)是一个非常流行的开源 Android 库,用于在您的 Android 应用程序中加载图像。根据官方文档,它指出:
...毕加索允许在您的应用程序中轻松加载图像 - 通常在一行代码中!
请注意,毕加索在后台使用 OkHttp (来自同一开发人员的网络库)通过 Internet 加载图像。
2. 那么为什么要使用毕加索呢?
现在您已经了解了毕加索的全部内容,您可能会问的下一个问题是为什么要使用它?
用 java 或 kotlin 开发自己的媒体加载和显示功能可能会很痛苦:您必须处理缓存、解码、管理网络连接、线程、异常处理等。Picasso 是一个易于使用、计划周密、文档完善且经过全面测试的库,它可以为您节省大量宝贵的时间,并为您省去一些麻烦。
根据官方文档,以下是 Picasso 为您处理的在 Android 上加载图像的许多常见陷阱:
ImageView 在适配器中处理 回收和下载取消
使用最少内存的复杂图像变换
自动内存和磁盘缓存
将图像添加到您的应用程序可以使您的 Android 应用程序活跃起来。因此,在本教程中,我们将通过构建一个简单的图片库应用程序来了解 Picasso 2。它将通过互联网加载图像并将它们显示为 recyclerview中的缩略图,当用户单击图像时,它将打开一个包含更大图像的详细活动。
可以在我们的 GitHub 存储库中找到本教程的示例项目(在 Kotlin 中),以便您轻松跟进。
好的艺术家抄袭,伟大的艺术家偷窃。- 巴勃罗毕加索
3.先决条件
为了能够遵循本教程,您需要:
对核心 Android api和Kotlin有基本的了解
Android Studio 3.1.1 或更高版本
Kotlin 插件 1.2.30 或更高版本
启动 Android Studio 并创建一个新项目(您可以命名它 PicassoDemo),其中包含一个名为 MainActivity. 确保还选中 包含 Kotlin 支持 复选框。
4. 声明依赖
创建新项目后,在 build.gradle中指定以下依赖项。在撰写本文时,毕加索的最新版本是 2.71828.
dependencies { implementation 'com.android.support:recyclerview-v7:27.1.1' implementation 'com.squareup.picasso:picasso:2.71828' }
或者使用 Maven:
<dependency> <groupId>com.squareup.picasso</groupId> <artifactId>picasso</artifactId> <version>2.71828</version> </dependency>
RecyclerView确保在添加毕加索和v7 工件后同步您的项目。
5. 添加上网权限
因为 Picasso 将执行网络请求以通过 Internet 加载图像,所以我们需要INTERNET在AndroidManifest.xml中包含权限。
所以现在就去做吧!
<uses-permission android:name="android.permission.INTERNET" />
请注意,仅当您要从 Internet 加载图像时才需要d。如果您仅在设备上本地加载图像,则不需要这样做。
6. 创建布局
我们将从创建我们 RecyclerView的 activity_main.xml 布局文件开始。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.RecyclerView android:id="@+id/rv_images" android:layout_width="match_parent" android:layout_height="match_parent"/> </RelativeLayout>
创建自定义项目布局
接下来,让我们创建 XML 布局 ( item_image.xmlImageView ) ,它将用于RecyclerView.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView android:id="@+id/iv_photo" android:adjustViewBounds="true" android:layout_height="200dp" android:scaleType="centerCrop" android:layout_margin="2dp" android:layout_width="match_parent"/> </LinearLayout>
现在我们已经创建了简单图库应用所需的布局,下一步是创建 用于填充数据RecyclerView的 适配器。不过,在我们这样做之前,让我们创建简单的数据模型。
7. 创建数据模型
我们将为我们的 RecyclerView. 该模型实现 了Parcelable ,用于在 Android 中将数据从一个组件高性能传输到另一个组件。在我们的例子中,数据将从 传输 SunsetGalleryActivity到 SunsetPhotoActivity。
data class SunsetPhoto(val url: String) : Parcelable { constructor(parcel: Parcel) : this(parcel.readString()) override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeString(url) } override fun describeContents(): Int { return 0 } companion object CREATOR : Parcelable.Creator<SunsetPhoto> { override fun createFromParcel(parcel: Parcel): SunsetPhoto { return SunsetPhoto(parcel) } override fun newArray(size: Int): Array<SunsetPhoto?> { return arrayOfNulls(size) } } }
请注意,此模型SunsetPhoto只有一个名为url (用于演示目的)的字段,但如果您愿意,您可以拥有更多。这个类实现Parcelable了,这意味着我们必须重写一些方法。
我们可以利用 Android Studio IDEA 为我们生成这些方法,但这样做的缺点是维护。如何?每当我们向这个类添加任何新字段时,我们可能会忘记显式更新和方法constructor,writeToParcel如果我们不更新方法可能会导致一些错误。
现在,为了避免更新或编写这些样板方法,Kotlin 1.1.14 引入了@Parcelize注解。这个注解将帮助我们在后台为我们自动生成writeToParcel、writeFromParcel和describeContents方法。
@Parcelize data class SunsetPhoto(val url: String) : Parcelable
现在,我们的代码SunsetPhoto类只有两行!惊人的!
请记住将以下代码添加到您的应用程序模块中 build.gradle:
androidExtensions { experimental = true }
此外,我 getSunsetPhotos() 在 SunsetPhoto模型类中包含了一个伴生对象(或 Java 中的静态方法),它在调用时将简单地返回一个ArrayListof SunsetPhoto。
@Parcelize data class SunsetPhoto(val url: String) : Parcelable { companion object { fun getSunsetPhotos(): Array<SunsetPhoto> { return arrayOf<SunsetPhoto>(SunsetPhoto("https://goo.gl/32YN2B"), SunsetPhoto("https://goo.gl/Wqz4Ev"), SunsetPhoto("https://goo.gl/U7XXdF"), SunsetPhoto("https://goo.gl/ghVPFq"), SunsetPhoto("https://goo.gl/qEaCWe"), SunsetPhoto("https://goo.gl/vutGmM")) } } }
8. 创建适配器
我们将创建一个适配器来填充我们 RecyclerView的数据。我们还将实现一个单击***器来打开详细信息活动SunsetPhotoActivity——将一个实例SunsetPhoto作为额外的意图传递给它。细节活动将显示图像的特写。我们将在后面的部分中创建它。
class MainActivity : AppCompatActivity() { //... private inner class ImageGalleryAdapter(val context: Context, val sunsetPhotos: Array<SunsetPhoto>) : RecyclerView.Adapter<ImageGalleryAdapter.MyViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageGalleryAdapter.MyViewHolder { val context = parent.context val inflater = LayoutInflater.from(context) val photoView = inflater.inflate(R.layout.item_image, parent, false) return MyViewHolder(photoView) } override fun onBindViewHolder(holder: ImageGalleryAdapter.MyViewHolder, position: Int) { val sunsetPhoto = sunsetPhotos[position] val imageView = holder.photoImageView } override fun getItemCount(): Int { return sunsetPhotos.size } inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener { var photoImageView: ImageView = itemView.findViewById(R.id.iv_photo) init { itemView.setOnClickListener(this) } override fun onClick(view: View) { val position = adapterPosition if (position != RecyclerView.NO_POSITION) { val sunsetPhoto = sunsetPhotos[position] val intent = Intent(context, SunsetPhotoActivity::class.java).apply { putExtra(SunsetPhotoActivity.EXTRA_SUNSET_PHOTO, sunsetPhoto) } startActivity(intent) } } } } }
请注意,我们使用apply 扩展函数将对象作为意图的额外对象。提醒一下,该apply函数返回作为参数传递给它的对象(即接收者对象)。
9. 从 URL 加载图像
我们将需要毕加索在本节中完成它的工作——不是为我们画一件艺术品,而是从互联网上获取图像并展示它们。 当用户滚动应用程序 时,我们将在我们的方法中分别在它们各自ImageView的 s 中显示这些图像。RecyclerView onBindViewHolder()
override fun onBindViewHolder(holder: ImageGalleryAdapter.MyViewHolder, position: Int) { val sunsetPhoto = sunsetPhotos[position] val imageView = holder.photoImageView Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .into(imageView) }
一步一步,这里是调用Picasso正在做的事情:
get()方法_
Picasso这将返回使用以下默认配置初始化 的全局实例(单例实例):
LRU 内存缓存 15% 的可用应用程序内存。
2%存储空间的磁盘缓存最大50MB但不小于5MB。注意:这仅适用于 API 14+。
三个用于磁盘和网络访问的下载线程。
请注意,如果这些设置不符合您的应用程序的要求,您可以Picasso使用Picasso.Builder.
val picassoBuilder = Picasso.Builder(context) // do custom configurations // Specify the {@link Downloader} that will be used for downloading images. picassoBuilder.downloader() // Specify the ExecutorService for loading images in the background. picassoBuilder.executor() // Specify the memory Cache used for the most recent images. picassoBuilder.memoryCache() // and more val picasso = picassoBuilder.build()
最后,您调用该build()方法返回一个Picasso具有您自己的配置的实例。
建议您在您的方法中执行此操作Application.onCreate,然后使用Picasso.setSingletonInstance该方法将其设置为单例实例,以确保该Picasso实例是全局实例。
load()方法 _
load(String path) 使用指定路径启动图像请求。此路径可以是远程 URL、文件资源、内容资源或 Android 资源。
placeholder(int placeholderResId):在加载并显示图像时使用的本地占位符资源 id 或可绘制对象。在下载图像时显示占位符图像是一种良好的用户体验。
请注意,Picasso 首先检查请求的图像是否在内存缓存中,如果是,则从那里显示图像(我们将在后面的部分中讨论 Picasso 中的缓存)。
其他方法
error(int errorResId):如果无法加载请求的图像,则使用可绘制对象(可能是因为网站已关闭)。
noFade():毕加索总是淡入要显示的图像ImageView。如果您不想要这种淡入动画,只需调用该noFade()方法即可。
into(ImageView imageView):将放置图像的目标图像视图。
图像大小调整和转换
如果您请求图像的服务器没有为您提供所需大小的图像,您可以使用resize(int targetWidth, int targetHeight). 调用此方法调整图像大小,然后将其显示在ImageView. 请注意,尺寸以像素 (px) 为单位,而不是 dp。
Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .resize(400, 200) .into(imageView)
您可以使用 方法传入宽度和高度的 Android 维度资源 resizeDimen(int targetWidthResId, int targetHeightResId)。此方法会将维度大小转换为原始像素,然后resize()在后台调用——将转换后的大小(以像素为单位)作为参数传递。
Picasso.get() //... .resizeDimen(R.dimen.list_detail_image_size, R.dimen.list_detail_image_size) //...
请注意,这些调整大小的方法不会考虑纵横比。换句话说,您的图像纵横比可能会失真。
幸运的是,Picasso给了我们一些有用的方法来解决这个问题:
centerCrop():均匀缩放图像(保持图像的纵横比),使图像填满给定区域,尽可能多地显示图像。如果需要,图像将被水平或垂直裁剪以适合。调用此方法会在 指定的范围内裁剪图像resize()。
centerInside():缩放图像,使两个尺寸都等于或小于请求的边界。这将使图像在 指定的范围内居中resize()。
onlyScaleDown():仅当原始图像尺寸大于指定的目标尺寸时才调整图像大小resize()。
fit(): 尝试调整图像大小以完全适合目标ImageView的边界。
图像旋转
Picasso 有一个简单的 API 可以旋转图像然后显示该图像。该 rotate(float degrees) 方法将图像旋转指定的度数。
Picasso.get() //... .rotate(90f) //...
在上面的示例中,这会将图像旋转 90 度。该 rotate(float degrees, float pivotX, float pivotY) 方法将图像围绕枢轴点旋转指定的度数。
Picasso.get() //... .rotate(30f, 200f, 100f) //...
在这里,我们将围绕枢轴点 200、100 像素将图像旋转 30 度。
转型
除了通过旋转来操作图像之外,毕加索还为我们提供了在显示图像之前对图像应用自定义转换的选项。
您只需创建一个实现 Picasso transformation接口的类。然后,您必须覆盖两种方法:
Bitmap transform(Bitmap source):这会将源位图转换为新位图。
String key():返回转换的唯一键,用于缓存目的。
创建自定义转换后,您只需调用transform(Transformation transformation)Picasso 实例即可执行它。请注意,您还可以传递Transformationto 的列表transform()。
Picasso.get() // ... .transform(CropCircleTransformation()) .into(imageView)
在这里,我对Picasso Transformations开源 Android 库中的图像应用了圆形裁剪变换。这个库有许多转换,您可以使用 Picasso 应用到图像 - 包括用于模糊或灰度图像的转换。如果您想对图像应用一些很酷的转换,请查看它。
10. 初始化适配器
在这里,我们简单地创建我们的 RecyclerView with GridLayoutManager 作为布局管理器,初始化我们的适配器,并将其绑定到 RecyclerView.
class MainActivity : AppCompatActivity() { private lateinit var recyclerView: RecyclerView private lateinit var imageGalleryAdapter: ImageGalleryAdapter override fun onCreate(savedInstanceState: Bundle?) { //... val layoutManager = GridLayoutManager(this, 2) recyclerView = findViewById(R.id.rv_images) recyclerView.setHasFixedSize(true) recyclerView.layoutManager = layoutManager imageGalleryAdapter = ImageGalleryAdapter(this, SunsetPhoto.getSunsetPhotos()) } override fun onStart() { super.onStart() recyclerView.adapter = imageGalleryAdapter } // ... }
11. 创建详细活动
创建一个新活动并命名它 SunsetPhotoActivity。我们得到 SunsetPhoto 额外的并像 onStart()以前一样使用 Picasso 加载图像。
class SunsetPhotoActivity : AppCompatActivity() { companion object { const val EXTRA_SUNSET_PHOTO = "SunsetPhotoActivity.EXTRA_SUNSET_PHOTO" } private lateinit var imageView: ImageView private lateinit var sunsetPhoto: SunsetPhoto override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_sunset_photo) sunsetPhoto = intent.getParcelableExtra(EXTRA_SUNSET_PHOTO) imageView = findViewById(R.id.image) } override fun onStart() { super.onStart() Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .into(imageView) } }
详细布局
这是显示详细活动的布局。它只显示一个 ImageView 将显示加载图像的全分辨率版本。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/image" android:layout_width="match_parent" android:layout_height="wrap_content" android:adjustViewBounds="true" android:scaleType="fitCenter" android:layout_gravity="center"/> </LinearLayout>
12. Picasso 中的缓存机制
如果您仔细观察,您会注意到,当您重新访问之前加载的图像时,它的加载速度甚至比以前更快。是什么让它更快?这是毕加索的缓存机制,就是这样。
这是引擎盖下发生的事情。从 Internet 加载一次图像后,Picasso 会将其缓存在内存和磁盘中,从而节省重复的网络请求并允许更快地检索图像。当再次需要该图像时,Picasso 将首先检查该图像是否在内存中可用,如果存在,它将立即返回。如果该图像不在内存中,毕加索接下来将检查磁盘,如果它在那里,它会返回它。如果它不存在,毕加索最终会对该图像进行网络请求并显示它。
总而言之,这是图像请求的(幕后)发生的事情:内存 -> 磁盘 -> 网络。
但是,根据您的应用程序,您可能希望避免缓存——例如,如果显示的图像可能经常更改并且不会重新加载。
那么如何禁用缓存呢?
您可以通过调用来避免内存缓存 memoryPolicy(MemoryPolicy.NO_CACHE)。这将在处理图像请求时简单地跳过内存缓存查找。
Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .memoryPolicy(MemoryPolicy.NO_CACHE) .into(imageView)
请注意,还有另一个枚举:MemoryPolicy.NO_STORE. 如果您非常确定您只会请求一次图像,这将非常有用。应用它也不会将图像存储在内存缓存中,从而不会从内存缓存中强制删除其他位图。
但请注意,图像仍将缓存在磁盘上——以防万一,您也使用 networkPolicy(@NonNull NetworkPolicy policy, @NonNull NetworkPolicy... additional),它采用以下一个或多个枚举值:
NetworkPolicy.NO_CACHE: 跳过检查磁盘缓存并强制通过网络加载。
NetworkPolicy.NO_STORE: 跳过将结果存储到磁盘缓存中。
NetworkPolicy.OFFLINE:强制请求仅通过磁盘缓存,跳过网络。
要完全避免内存和磁盘缓存,只需依次调用这两种方法:
Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .memoryPolicy(MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE) .networkPolicy(NetworkPolicy.NO_CACHE) .into(imageView)
13. 请求监听器
在 Picasso 中,您可以实现***器或回调来监控您在图像加载时发出的请求的状态。Target如果您在请求上 实现接口,则只会调用其中一种方法。
void onBitmapFailed(e: Exception?, errorDrawable: Drawable?): 当图像无法成功加载时触发。在这里,我们可以访问引发的异常。
void onBitmapLoaded(Bitmap bitmap, LoadedFrom from): 每当成功加载图像时触发。在这里,我们获取位图以显示给用户。
void onPrepareLoad(Drawable placeHolderDrawable):在您提交请求之前调用。
Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .into(object : Target { override fun onPrepareLoad(placeHolderDrawable: Drawable?) { } override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) { } override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) { } })
如果您有进度对话框,您还可以在此处显示然后隐藏进度对话框。
如果需要,您可以实现另一个回调***器,称为callback. 这个接口只有两个方法: onSuccess()和 onError(Exception e). 前者在图片请求加载成功时调用,后者在处理请求出错时调用。
回到我们的图片库应用程序(内部SunsetPhotoActivity),让我们使用一个对象来稍微修改显示,该对象Callback将位图设置 ImageView 为 调色板 API。
因此,在您的应用模块的build.gradle中包含调色板工件:
dependencies { //... implementation 'com.android.support:palette-v7:27.1.1' }
现在让我们Callback在 Picasso 请求中实现接口。
override fun onStart() { super.onStart() Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .into(imageView, object : Callback { override fun onSuccess() { val bitmap = (imageView.drawable as BitmapDrawable).bitmap onPalette(Palette.from(bitmap).generate()) } override fun onError(e: Exception?) { } }) } fun onPalette(palette: Palette?) { if (null != palette) { val parent = imageView.parent.parent as ViewGroup parent.setBackgroundColor(palette.getDarkVibrantColor(Color.GRAY)) } }
14. 测试应用程序
最后,您可以运行应用程序了!单击缩略图以获取图像的全尺寸版本。
15. 优先处理请求
当您想在同一屏幕上同时加载不同的图像时,您可以选择排序哪个比另一个更重要。换句话说,您可以先加载重要的图像。
您只需调用priority()您的 Picasso 请求实例并传入任何枚举:Priority.LOW、Priority.NORMAL或 Priority.HIGH。
Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .priority(Picasso.Priority.HIGH) .into(imageView) Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .priority(Picasso.Priority.NORMAL) .into(imageView) Picasso.get() .load(sunsetPhoto.url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .priority(Picasso.Priority.LOW) .into(imageView)
16. 标记请求
通过标记您的毕加索请求,您可以恢复、暂停或取消与特定标记关联的请求。根据您的用例,您可以使用应将请求范围定义为 a Context、 anActivity或 a的字符串或对象来标记您的请求Fragment。您可以通过调用一个来轻松标记毕加索请求tag(@NonNull Object tag)。Object将其作为标签 的实例传递给它 。
以下是您可以对标记的毕加索请求执行的以下操作:
pauseTag(Object tag): 暂停与给定标签关联的所有请求。
resumetag(Object tag):使用给定标签恢复暂停的请求。
cancelTag(Object tag):取消任何具有给定标签的现有请求。
Picasso.get() // ... .tag(context
尽管标记您的请求可以让您对请求进行一些控制,但在使用标记时应该非常小心,因为可能存在内存泄漏。这是官方文档所说的:
只要此标签暂停和/或有活动请求,毕加索就会保留对该标签的引用。注意潜在的泄漏。
从文件系统加载
在您的应用程序中本地加载图像很简单。
File file = new File("your/pic/file/path/file.png") Picasso.get() .load(file) .fit() .into(imageView)
结论
不错的工作!在本教程中,您已经使用 Picasso 构建了一个完整的图片库应用程序,并且在此过程中您了解了该库的工作原理以及如何将其集成到您自己的项目中。
您还学习了如何同时显示本地和远程图像、标记请求、确定请求的优先级以及如何应用图像转换(如调整大小)。不仅如此,您还看到了启用和禁用缓存、错误处理和自定义请求***器是多么容易。
- 创建自定义项目布局
- get()方法_
- load()方法 _
- 其他方法
- 图像大小调整和转换
- 图像旋转
- 转型
- 详细布局
- 那么如何禁用缓存呢?
- 从文件系统加载