1. build.gradle, AndroidManifast.xml, 配置文件添加引用
1.1 dependencies 中添加引用库
dependencies {
//http 请求
implementation 'com.android.volley:volley:1.2.1'
//上拉刷新
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
//加载网络地址图片
implementation 'com.github.bumptech.glide:glide:4.13.2'
annotationProcessor 'com.github.bumptech.glide:compiler:4.13.2'
def nav_version = "2.5.2"
//Kotlin navigation 导航
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
//gson 序列化
implementation 'com.google.code.gson:gson:2.9.1'
//加载图片时动画效果
implementation 'io.supercharge:shimmerlayout:2.1.0'
//图片缩放操作
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
}
1.2 添加 com.github.chrisbanes:PhotoView:2.3.0 库,settings.gradle 需要添加引用
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
//添加引用
maven { url "https://www.jitpack.io" }
}
}
1.3 视图与类绑定
android {
buildFeatures {
viewBinding = true
}
}
1.4 添加 @Parcelize 注解, 不用实现 Parcelable 里的方法
plugins {
id 'kotlin-parcelize'
}
1.5 AndroidManifest.xml 添加网络权限
<uses-permission android:name="android.permission.INTERNET" />
<application android:usesCleartextTraffic="true">
1.6 占位图 ic_baseline_photo_24.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#D8D9D9"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z" />
</vector>
1.7 占位图 ic_photo_place_holder.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<solid android:color="#99999999" />
<size android:width="40dp" android:height="30dp" />
</shape>
</item>
</selector>
1.8 占位图喜欢 ic_baseline_thumb_up.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:tint="#F50606"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M1,21h4L5,9L1,9v12zM23,10c0,-1.1 -0.9,-2 -2,-2h-6.31l0.95,-4.57 0.03,-0.32c0,-0.41 -0.17,-0.79 -0.44,-1.06L14.17,1 7.59,7.59C7.22,7.95 7,8.45 7,9v10c0,1.1 0.9,2 2,2h9c0.83,0 1.54,-0.5 1.84,-1.22l3.02,-7.05c0.09,-0.23 0.14,-0.47 0.14,-0.73v-2z" />
</vector>
1.9 占位图收藏 ic_baseline_favorite.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:tint="#F50606"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z" />
</vector>
2. 创建 Gallery 主页
2.1 布局文件 fragment_gallery.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/swipeLayoutGallery"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="2dp"
tools:context=".GalleryFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
2.2 创建实体类,存放字段 Pixabay.kt
data class Pixabay(
val total: Int,
val totalHits: Int,
val hits: Array<PhotoItem>
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Pixabay
if (total != other.total) return false
if (totalHits != other.totalHits) return false
if (!hits.contentEquals(other.hits)) return false
return true
}
override fun hashCode(): Int {
var result = total
result = 31 * result + totalHits
result = 31 * result + hits.contentHashCode()
return result
}
}
//@Parcelize:注解 不用实现 Parcelable 里的方法
@Parcelize data class PhotoItem(
@SerializedName("id") val photoId: Int,
@SerializedName("webformatURL") val previewUrl: String,
@SerializedName("largeImageURL") val fullUrl: String,
@SerializedName("webformatHeight") val photoHeight: Int,
@SerializedName("user") val photoUser:String,
@SerializedName("likes") val photoLikes: Int,
@SerializedName("collections") val photoFavorites: Int
):Parcelable
2.3 创建 RecyclerView 子 View,gallery_cell.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="2dp"
app:cardCornerRadius="2dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_user"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="TextView"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/shimmerLayoutCell" />
<ImageView
android:id="@+id/imageView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_user"
app:srcCompat="@drawable/ic_baseline_thumb_up" />
<TextView
android:id="@+id/tv_likes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="TextView"
android:textSize="10sp"
app:layout_constraintBottom_toBottomOf="@+id/imageView2"
app:layout_constraintStart_toEndOf="@+id/imageView2"
app:layout_constraintTop_toTopOf="@+id/imageView2" />
<ImageView
android:id="@+id/imageView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="@+id/tv_likes"
app:layout_constraintStart_toEndOf="@+id/tv_likes"
app:layout_constraintTop_toTopOf="@+id/tv_likes"
app:srcCompat="@drawable/ic_baseline_favorite" />
<TextView
android:id="@+id/tv_favorites"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="TextView"
android:textSize="10sp"
app:layout_constraintBottom_toBottomOf="@+id/imageView3"
app:layout_constraintStart_toEndOf="@+id/imageView3"
app:layout_constraintTop_toTopOf="@+id/imageView3" />
<io.supercharge.shimmerlayout.ShimmerLayout
android:id="@+id/shimmerLayoutCell"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
tools:srcCompat="@tools:sample/avatars" />
</io.supercharge.shimmerlayout.ShimmerLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
2.4 创建 RecyclerView 适配器, GalleryAdapter.kt
class GalleryAdapter: ListAdapter<PhotoItem, MyViewHolder>(DIFFCALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val holder = MyViewHolder(
GalleryCellBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
holder.itemView.setOnClickListener {
Bundle().apply {
//putParcelable("PHOTO", getItem(holder.adapterPosition))
//holder.itemView.findNavController().navigate(R.id.action_galleryFragment_to_photoFragment,this)
//val list = ArrayList(snapshot())
putParcelableArrayList("PHOTO_LIST", ArrayList(currentList))
//putParcelableArrayList("PHOTO_LIST", ArrayList(snapshot()))
putInt("PHOTO_POSITION", holder.adapterPosition)
holder.itemView.findNavController().navigate(R.id.action_galleryFragment_to_pagerPhotoFragment, this)
}
}
return holder
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val photoItem = getItem(position)?: return
val cellBinding = holder.binding as GalleryCellBinding
with(cellBinding){
shimmerLayoutCell.apply {
setShimmerColor(0x55FFFFFF)
setShimmerAngle(0)
startShimmerAnimation()
}
tvUser.text = photoItem.photoUser
tvLikes.text = photoItem.photoLikes.toString()
tvFavorites.text = photoItem.photoFavorites.toString()
imageView.layoutParams.height = photoItem.photoHeight
}
Glide.with(holder.itemView)
.load(getItem(position)?.previewUrl)
.placeholder(R.drawable.ic_photo_place_holder)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
return false.also { cellBinding.shimmerLayoutCell.stopShimmerAnimation() }
}
})
.into(cellBinding.imageView)
}
object DIFFCALLBACK : DiffUtil.ItemCallback<PhotoItem>() {
override fun areItemsTheSame(oldItem: PhotoItem, newItem: PhotoItem): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: PhotoItem, newItem: PhotoItem): Boolean {
return oldItem.photoId == newItem.photoId
}
}
}
class MyViewHolder(binding: ViewBinding) : ViewHolder(binding.root) {
val binding = binding
}
2.5 封装网络请求类,VolleySingleton.kt
//单例模式
class VolleySingleton private constructor(context: Context) {
companion object {
private var INSTANCE: VolleySingleton? = null
fun getInstance(context: Context) =
INSTANCE ?: synchronized(this) {
VolleySingleton(context).also { INSTANCE = it }
}
}
val requestQueue: RequestQueue by lazy {
Volley.newRequestQueue(context.applicationContext)
}
}
2.6 创建菜单栏布局,刷新图片内容 menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/swipeIndicator"
android:title="swipe to refresh" />
</menu>
2.7 创建 ViewModel 监听数据,GalleryViewModel.kt
class GalleryViewModel(application: Application) : AndroidViewModel(application) {
private val TAG = GalleryViewModel::class.java.simpleName
private var _photoListLive = MutableLiveData<List<PhotoItem>>()
val photoListLive: LiveData<List<PhotoItem>>
get() = _photoListLive
fun fetchData() {
val stringRequest = StringRequest(
Request.Method.GET,
getUrl(),
{
_photoListLive.value = Gson().fromJson(it, Pixabay::class.java).hits.toList();
},
{
Log.e(TAG, "onErrorResponse", it)
}
)
VolleySingleton.getInstance(getApplication()).requestQueue.add(stringRequest)
}
private fun getUrl(): String {
//&per_page=100
return "https://pixabay.com/api/?key=30070990-cfc31c9f778ceeef4009d910d&q=${keyWords.random()}"
}
private val keyWords =
arrayOf("cat", "dog", "car", "beauty", "phone", "computer", "flower", "animal")
}
2.8 主页调用 GalleryFragment.kt
class GalleryFragment : Fragment() {
private lateinit var binding: FragmentGalleryBinding
private lateinit var galleryViewModel: GalleryViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
binding = FragmentGalleryBinding.inflate(inflater, container, false)
return binding.root
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.swipeIndicator -> {
binding.swipeLayoutGallery.isRefreshing = true
Handler().postDelayed(Runnable { galleryViewModel.fetchData() }, 700)
}
}
return super.onOptionsItemSelected(item)
}
//加载菜单栏
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu, menu)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setHasOptionsMenu(true)
val galleryAdapter = GalleryAdapter()
binding.recyclerView?.apply {
adapter = galleryAdapter
//GridLayouStaager(requireContext(), 2) 对齐 / StaggeredGridLayoutManager 交错感
layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
}
//,ViewModelProvider.AndroidViewModelFactory(requireActivity().application)
galleryViewModel = ViewModelProvider(this)[GalleryViewModel::class.java]
galleryViewModel.photoListLive.observe(viewLifecycleOwner, Observer {
galleryAdapter.submitList(it)
binding.swipeLayoutGallery.isRefreshing = false
})
galleryViewModel.photoListLive.value ?: galleryViewModel.fetchData()
binding.swipeLayoutGallery?.setOnRefreshListener {
galleryViewModel.fetchData()
}
}
}
3. Photo 详情页
3.1 布局文件 fragment_photo.xml
<?xml version="1.0" encoding="utf-8"?>
<io.supercharge.shimmerlayout.ShimmerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/shimmerLayoutPhoto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".PhotoFragment" >
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/photoView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
tools:src="@tools:sample/avatars" />
</io.supercharge.shimmerlayout.ShimmerLayout>
3.2 调用类,PhotoFragment.kt
class PhotoFragment : Fragment() {
private lateinit var binding: FragmentPhotoBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
binding = FragmentPhotoBinding.inflate(inflater, container, false);
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.shimmerLayoutPhoto.apply {
setShimmerColor(0x55FFFFFF)
setShimmerAngle(0)
startShimmerAnimation()
}
val item = arguments?.getParcelable<PhotoItem>("PHOTO")
Glide.with(requireContext()).load(item?.fullUrl)
.placeholder(R.drawable.ic_baseline_photo_24)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
return false.also { binding.shimmerLayoutPhoto.stopShimmerAnimation() }
}
}).into(binding.photoView)
}
}
4. Activity 调用
4.1 Fragment 导航布局, navigation.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/navigation"
app:startDestination="@id/galleryFragment">
<fragment
android:id="@+id/galleryFragment"
android:name="com.example.gallery.GalleryFragment"
android:label="Gallery"
tools:layout="@layout/fragment_gallery">
<action
android:id="@+id/action_galleryFragment_to_photoFragment"
app:destination="@id/photoFragment" />
</fragment>
<fragment
android:id="@+id/photoFragment"
android:name="com.example.gallery.PhotoFragment"
android:label="Photo"
tools:layout="@layout/fragment_photo" />
</navigation>
4.2 布局文件 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
4.3 调用页 MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var controller:NavHostFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//添加返回按键
controller = supportFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment
NavigationUI.setupActionBarWithNavController(this,controller.navController)
}
//返回按键监听
override fun onSupportNavigateUp(): Boolean {
return super.onSupportNavigateUp()||controller.navController.navigateUp()
}
}
5. 效果图