准备工作
首先导包
implementation 'androidx.navigation:navigation-fragment:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
implementation("com.squareup.okhttp3:okhttp:4.9.0")
implementation("com.squareup.okio:okio:2.2.2")
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'com.github.bumptech.glide:glide:4.13.2'
annotationProcessor 'com.github.bumptech.glide:compiler:4.13.2'
api 'com.readystatesoftware.systembartint:systembartint:1.0.3'
implementation("com.tencent:mmkv:1.2.13")
implementation 'io.github.scwang90:refresh-layout-kernel:2.0.5' //核心必须依赖
implementation 'io.github.scwang90:refresh-header-classics:2.0.5' //经典刷新头
implementation 'io.github.scwang90:refresh-footer-classics:2.0.5' //经典加载
在gradle.properties添加如下一行
android.enableJetifier=true
然后同步一下
创建一个navigation用来管理我们的fragment
添加network_security_config允许我们的网络请求
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
创建一个BaseFragment作为fragment的基类,代码如下
abstract class BaseFragment<T : ViewBinding> : Fragment() {
protected lateinit var mBinding: T
protected lateinit var mainModel: MainModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mainModel = ViewModelProvider(requireActivity()).get(MainModel::class.java)
mBinding = providedViewBinding(inflater, container)
initData()
initEvent()
return mBinding.root
}
abstract fun providedViewBinding(inflater: LayoutInflater, container: ViewGroup?): T
abstract fun initData()
abstract fun initEvent()
}
再创建MainModel继承ViewModel用来管理我们的数据
class MainModel : ViewModel() {
}
获取网络数据
创建一个HomeFragment继承BaseFragment作为我们的首页
class HomeFragment : BaseFragment<FragmentHomeBinding>() {
override fun providedViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeBinding {
return FragmentHomeBinding.inflate(inflater, container, false)
}
override fun initData() {
}
override fun initEvent() {
}
}
在nav_graph.xml中添加该布局
在编写activity_main.xml 的代码,引入该navigation
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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/fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
</RelativeLayout>
MainActivity可以暂时关掉,开始编写HomeFragment中的逻辑
首先我们要先获取壁纸的分类,调用的接口如下
http://service.picasso.adesk.com/v1/vertical/category?adult=true&first=1
可以自己调用以下看看是否可用,如下
复制这串json转成实体类
用到的插件是这个
转换成功如下所示
接着我们在代码中调用
在HomeFragment中编写如下代码,用来获取我们网络请求的数据
private val list = mutableListOf<CategoryX>()
override fun initData() {
val url = "$BASE_URL?adult=true&first=1"
val request: Request = Request.Builder()
.url(url)
.method("GET", null)
.build()
OkHttpClient().newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.d(TAG, "onFailure: ")
}
@SuppressLint("NotifyDataSetChanged")
override fun onResponse(call: Call, response: Response) {
if (response.code == 200) {
val string = response.body?.string()
val result = Gson().fromJson(string, Category::class.java)
list.clear()
if (result?.res?.category != null) {
result.res.category.forEach {
list.add(it)
}
}
Log.d(TAG, "onResponse: $list")
}
}
})
}
const val BASE_URL = "http://service.picasso.adesk.com/v1/vertical/category"
接着运行一下,结果如图证明请求返回的数据保存成功,如果失败接口没问题的话八成是权限的问题
分类不同类型的壁纸
其次我们需要一个左右滚动的viewpager2用来存放不同分类的壁纸的fragment
然后创建一个CategoryFragment用来显示不同分类的壁纸
class CategoryFragment(private val id: String) : BaseFragment<FragmentCategoryBinding>() {
override fun providedViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentCategoryBinding {
return FragmentCategoryBinding.inflate(inflater, container, false)
}
override fun initData() {
}
override fun initEvent() {
}
}
这里有个id的成员变量是作为调用分类接口的参数,接口如下,
http://service.picasso.adesk.com/v1/vertical/category/4e4d610cdf714d2966000003/vertical?limit=30&skip=180&adult=false&first=1&order=new
limit:返回的数据条数
skip:跳过的个数
adult:这个我试过没用的,可惜
现在我们写一个adapter把HomeFragment的数据传过来
class CategoryAdapter(fragmentActivity: FragmentActivity, private var list: MutableList<String>) : FragmentStateAdapter(fragmentActivity) {
override fun getItemCount(): Int {
return list.size
}
override fun createFragment(position: Int): Fragment {
return CategoryFragment(list[position])
}
}
接着在HomeFragment中,给viewpager设置该适配器,创建一个ids做为id列表,请求返回数据时更新该列表
mBinding.apply {
categoryAdapter = CategoryAdapter(requireActivity(), ids)
viewPager.adapter = categoryAdapter
}
private val ids = mutableListOf<String>()
private lateinit var categoryAdapter: CategoryAdapter
@SuppressLint("NotifyDataSetChanged")
override fun onResponse(call: Call, response: Response) {
if (response.code == 200) {
val string = response.body?.string()
val result = Gson().fromJson(string, Category::class.java)
list.clear()
ids.clear()
if (result?.res?.category != null) {
result.res.category.forEach {
list.add(it)
ids.add(it.id)
}
Handler(Looper.getMainLooper()).post {
categoryAdapter.notifyDataSetChanged()
}
}
Log.d(TAG, "onResponse: $list")
}
}
运行一下可以发现viewpager可以滑动了,可以数一下页数就是类型的种数
显示壁纸
重头戏了
现在我们调用一下刚才写过的接口去调试一下
然后复制一下json数据按同样的方式转成实体类
首先写一下fragment_category.xml中的代码,这里recyclerView配置上LayoutManager和SpanCount就不用在代码中写了
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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=".ui.CategoryFragment">
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.scwang.smart.refresh.header.ClassicsHeader
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="1dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="3" />
<com.scwang.smart.refresh.footer.ClassicsFooter
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
</FrameLayout>
接着创建一个item_picture作为上面recyclerview的item
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="1dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/iv_pic"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="centerCrop"
app:shapeAppearance="@style/img_corner_20dp" />
</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="img_corner_20dp">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">5dp</item>
</style>
</resources>
用ShapeableImageView的话可以加个圆角好看点
接着写一个PictureAdapter适配器
class PicAdapter(
private val context: Context,
private val list: MutableList<Vertical>
) : RecyclerView.Adapter<VH<ItemPictureBinding>>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH<ItemPictureBinding> {
val mBinding = ItemPictureBinding.inflate(LayoutInflater.from(context), parent, false)
return VH(mBinding)
}
override fun onBindViewHolder(holder: VH<ItemPictureBinding>, position: Int) {
holder.binding.apply {
Glide.with(context)
.load(list[holder.adapterPosition].thumb)
.into(ivPic)
}
}
override fun getItemCount(): Int {
return list.size
}
}
最后在CategoryFragment中使用
private lateinit var picAdapter: PicAdapter
private val list = mutableListOf<Vertical>()
private val limit = 30 //每次加载限制的个数
private var page = 0 //当前页数
override fun initData() {
picAdapter = PicAdapter(requireContext(), list)
mBinding.apply {
recyclerView.adapter = picAdapter
}
loadPic()
}
override fun initEvent() {
mBinding.apply {
refreshLayout.setOnRefreshListener {
page = 0
loadPic()
}
refreshLayout.setOnLoadMoreListener {
page++
loadPic()
}
}
}
@SuppressLint("NotifyDataSetChanged")
private fun loadPic() {
val random = Random(Date().time).nextInt(200)
Log.d(TAG, "loadPic: $random")
val url = "$BASE_URL/$id/vertical?limit=$limit&skip=${random * limit}&adult=false&first=1&order=new"
Log.d(TAG, "loadPic: $url")
httpGet(url) { success, msg ->
if (success) {
val picture = Gson().fromJson(msg, Picture::class.java)
val size = list.size
picture?.res?.vertical?.let {
if (page == 0) {
list.clear()
picAdapter.notifyDataSetChanged()
}
list.addAll(it)
}
picAdapter.notifyItemRangeInserted(size, limit)
} else {
Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show()
}
mBinding.refreshLayout.finishRefresh()
mBinding.refreshLayout.finishLoadMore()
}
}
此处加载的网络请求我封装了一个方法方便调用,可以单独放在一个工具类中使用
fun httpGet(url: String, callBack: (Boolean, String) -> Unit) {
Thread {
val request: Request = Request.Builder()
.url(url)
.get()
.build()
Log.d(TAG, "httpGet: $url")
OkHttpClient().newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Handler(Looper.getMainLooper()).post {
callBack(false, "error1")
}
}
override fun onResponse(call: Call, response: Response) {
if (response.body != null) {
val json = response.body!!.string()
val headers = response.networkResponse!!.request.headers
try {
Handler(Looper.getMainLooper()).post {
callBack(true, json)
}
} catch (e: Exception) {
Log.e(TAG, "httpGet: $e")
Handler(Looper.getMainLooper()).post {
callBack(false, "error3:$e")
}
}
} else {
Log.e(TAG, "httpGet: error2")
Handler(Looper.getMainLooper()).post {
callBack(false, "error2")
}
}
}
})
}.start()
}
同样,HomeFragment中的网络请求也可以用这个简化一下
override fun initData() {
loadCategory()
mBinding.apply {
categoryAdapter = CategoryAdapter(requireActivity(), ids)
viewPager.adapter = categoryAdapter
}
}
override fun initEvent() {
}
@SuppressLint("NotifyDataSetChanged")
private fun loadCategory() {
val url = "$BASE_URL?adult=true&first=1"
httpGet(url) { success, msg ->
if (success) {
val result = Gson().fromJson(msg, Category::class.java)
list.clear()
ids.clear()
if (result?.res?.category != null) {
result.res.category.forEach {
list.add(it)
ids.add(it.id)
}
categoryAdapter.notifyDataSetChanged()
}
} else {
Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show()
}
}
}
现在运行一下,不出意外可以成功显示了
界面美化
首先把顶部的actionbar去掉,实在太丑了,改一下下面的样式为NoActionBar
再次运行发现没有了,但是顶部状态栏还是有个很违和的颜色,这里使用别人写好的一个工具类去透明化
/**
* 状态栏工具类
*/
object StatusBarUtil {
/**
* 修改状态栏为全透明
*
* @param activity
*/
@TargetApi(19)
fun transparencyBar(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val window = activity.window
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE)
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
window.statusBarColor = Color.TRANSPARENT
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
val window = activity.window
window.setFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
)
}
}
/**
* 修改状态栏颜色,支持4.4以上版本
*
* @param activity
* @param colorId
*/
fun setStatusBarColor(activity: Activity, colorId: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val window = activity.window
// window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.statusBarColor = activity.resources.getColor(colorId)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
//使用SystemBarTint库使4.4版本状态栏变色,需要先将状态栏设置为透明
transparencyBar(activity)
val tintManager = SystemBarTintManager(activity)
tintManager.setStatusBarTintEnabled(true)
tintManager.setStatusBarTintResource(colorId)
}
}
/**
* 状态栏亮色模式,设置状态栏黑色文字、图标,
* 适配4.4以上版本MIUIV、Flyme和6.0以上版本其他Android
*
* @param activity
* @return 1:MIUUI 2:Flyme 3:android6.0
*/
fun StatusBarLightMode(activity: Activity): Int {
var result = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (MIUISetStatusBarLightMode(activity, true)) {
result = 1
} else if (FlymeSetStatusBarLightMode(activity.window, true)) {
result = 2
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activity.window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
result = 3
}
}
return result
}
/**
* 已知系统类型时,设置状态栏黑色文字、图标。
* 适配4.4以上版本MIUIV、Flyme和6.0以上版本其他Android
*
* @param activity
* @param type 1:MIUUI 2:Flyme 3:android6.0
*/
fun StatusBarLightMode(activity: Activity, type: Int) {
if (type == 1) {
MIUISetStatusBarLightMode(activity, true)
} else if (type == 2) {
FlymeSetStatusBarLightMode(activity.window, true)
} else if (type == 3) {
activity.window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
}
/**
* 状态栏暗色模式,清除MIUI、flyme或6.0以上版本状态栏黑色文字、图标
*/
fun StatusBarDarkMode(activity: Activity, type: Int) {
if (type == 1) {
MIUISetStatusBarLightMode(activity, false)
} else if (type == 2) {
FlymeSetStatusBarLightMode(activity.window, false)
} else if (type == 3) {
activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
}
}
/**
* 设置状态栏图标为深色和魅族特定的文字风格
* 可以用来判断是否为Flyme用户
*
* @param window 需要设置的窗口
* @param dark 是否把状态栏文字及图标颜色设置为深色
* @return boolean 成功执行返回true
*/
fun FlymeSetStatusBarLightMode(window: Window?, dark: Boolean): Boolean {
var result = false
if (window != null) {
try {
val lp = window.attributes
val darkFlag = WindowManager.LayoutParams::class.java
.getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON")
val meizuFlags = WindowManager.LayoutParams::class.java
.getDeclaredField("meizuFlags")
darkFlag.isAccessible = true
meizuFlags.isAccessible = true
val bit = darkFlag.getInt(null)
var value = meizuFlags.getInt(lp)
value = if (dark) {
value or bit
} else {
value and bit.inv()
}
meizuFlags.setInt(lp, value)
window.attributes = lp
result = true
} catch (e: Exception) {
}
}
return result
}
/**
* 需要MIUIV6以上
*
* @param activity
* @param dark 是否把状态栏文字及图标颜色设置为深色
* @return boolean 成功执行返回true
*/
fun MIUISetStatusBarLightMode(activity: Activity, dark: Boolean): Boolean {
var result = false
val window = activity.window
if (window != null) {
val clazz: Class<*> = window.javaClass
try {
var darkModeFlag = 0
val layoutParams = Class.forName("android.view.MiuiWindowManager\$LayoutParams")
val field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE")
darkModeFlag = field.getInt(layoutParams)
val extraFlagField = clazz.getMethod(
"setExtraFlags",
Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType
)
if (dark) {
extraFlagField.invoke(window, darkModeFlag, darkModeFlag) //状态栏透明且黑色字体
} else {
extraFlagField.invoke(window, 0, darkModeFlag) //清除黑色字体
}
result = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//开发版 7.7.13 及以后版本采用了系统API,旧方法无效但不会报错,所以两个方式都要加上
if (dark) {
activity.window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
} else {
activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
}
}
} catch (e: Exception) {
}
}
return result
}
}
在MainActivity中调用透明化的方法
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
StatusBarUtil.transparencyBar(this)
}
}
再次运行好看多了,当然也可以直接设置全屏,然后再recyclerview上下滑动时监听调用全屏的方法,按照自己的需求去取舍
图片加载成功的时候也可以加上动画,这里自己写了一个自定义view
class ScaleImage : ShapeableImageView {
constructor(context: Context?) : this(context, null)
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle)
private var num = Int.MAX_VALUE
private val count = 40f
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (num <= count) {
scaleX = num / count
scaleY = num / count
num++
}
invalidate()
}
fun startAnim() {
num = 0
}
companion object{
const val TAG = "ScaleImage"
}
}
核心就是继承ShapeableImageView,重写onDraw方法,在绘制时每次判断num条件,去增加scaleX和scaleY,startAnim方法就是将num置为0便可以播放动画
将item_picture.xml 中的ShapeableImageView换成这个ScaleImage
最后改一下PicAdapter适配器的bindviewholder方法:在glide加载图片里加上监听器加载成功时播放动画,加载失败时加了一个失败的图片
override fun onBindViewHolder(holder: VH<ItemPictureBinding>, position: Int) {
holder.binding.apply {
Glide.with(context)
.load(list[holder.adapterPosition].thumb)
.addListener(object : RequestListener<Drawable> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
ivPic.setImageResource(R.drawable.ic_baseline_broken_image_24)
return true
}
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
ivPic.startAnim()
ivPic.setImageDrawable(resource)
return true
}
})
.into(ivPic)
}
}
之后运行一下,ok,效果很好
再之后给图片点击加上水波纹,很简单加一个foreground属性就可以了
<com.zrq.nicepicture.view.ScaleImage
android:id="@+id/iv_pic"
android:layout_width="match_parent"
android:layout_height="200dp"
android:foreground="@drawable/pressed_background"
android:scaleType="centerCrop"
app:shapeAppearance="@style/img_corner_20dp" />
pressed_background.xml的代码,这里ripple的color字段中的颜色对应着水波纹的颜色,可以按自己喜欢去配置
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/white">
</ripple>
运行发现水波纹并没有生效,调试发现要给这个控件的点击加上监听才可以
PicAdapter加上如下代码,便可以了,这里的点击后面会回调出去的这里先给个空方法
ivPic.setOnClickListener { }
运行,可以正常显示
壁纸详情页
首先将item的点击事件响应回调出去
然后创建详情页的fragment
class PicFragment : BaseFragment<FragmentPicBinding>() {
override fun providedViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentPicBinding {
return FragmentPicBinding.inflate(inflater, container, false)
}
override fun initData() {
}
override fun initEvent() {
}
}
添加到navigation中,直接点上面的加号就行
改写PicAdapter调用时的构造方法,当点击时保存数据到viewModel,然后跳转到刚创建的fragment
picAdapter = PicAdapter(requireContext(), list) { _, pos ->
mainModel.list.clear()
mainModel.list.addAll(list)
mainModel.pos = pos
Navigation.findNavController(requireActivity(), R.id.fragment_container)
.navigate(R.id.picFragment)
}
此处使用了ViewModel进行数据的共享,如下
class MainModel : ViewModel() {
val list = mutableListOf<Vertical>()
var pos = 0
}
传递来的数据是一个列表,这里用viewpage2来接收,实现上下滑动翻页浏览壁纸的效果
用viewpager2的话还是要编写一个adapter,直接复制之前的改一下列表的数据类型和子fragment就可以了
class PicItemAdapter(
fragmentActivity: FragmentActivity,
private var list: MutableList<String>
) : FragmentStateAdapter(fragmentActivity) {
override fun getItemCount(): Int {
return list.size
}
override fun createFragment(position: Int): Fragment {
return PicItemFragment(list[position])
}
}
这个PicItemFragment就是显示壁纸大图的容器,所以需要接收图片的地址,从上面的adapter中传递给PicItemFragment的构造器
这里还做了点击返回简化操作手法
class PicItemFragment(private val url: String) : BaseFragment<FragmentPicItemBinding>() {
override fun providedViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentPicItemBinding {
return FragmentPicItemBinding.inflate(inflater, container, false)
}
override fun initData() {
mBinding.apply {
Glide.with(requireActivity())
.load(url)
.into(image)
}
}
override fun initEvent() {
mBinding.apply {
image.setOnClickListener {
Navigation.findNavController(requireActivity(), R.id.fragment_container)
.popBackStack()
}
}
}
}
其布局中就一个imageview,这里的fragment其实就是一个item,所以命名为PicItemFragment,当作之前写recylerview的item去编写,步骤都是相似的
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.PicItemFragment">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image"
android:foreground="@drawable/pressed_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop" />
</FrameLayout>
现在给这个PicItemAdapter实例化,就在之前创建的PicFragment中进行,数据集就从ViewModel中共享
class PicFragment : BaseFragment<FragmentPicBinding>() {
override fun providedViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentPicBinding {
return FragmentPicBinding.inflate(inflater, container, false)
}
private lateinit var adapter: PicItemAdapter
private val list = mutableListOf<String>()
override fun initData() {
adapter = PicItemAdapter(requireActivity(), list)
mBinding.apply {
viewPager.adapter = adapter
}
}
@SuppressLint("NotifyDataSetChanged")
override fun initEvent() {
list.clear()
mainModel.list.forEach {
list.add(it.img)
}
mBinding.viewPager.setCurrentItem(mainModel.pos, false)
adapter.notifyDataSetChanged()
}
}
这样一整个就串起来了,现在运行一下
详情页美化
现在逻辑是通了,之后来继续加点动效
首先就是跳转的动画,连一条线从homeFragment到picFragment,然后加两个字段enterAnim和popExitAnim
这里在res文件夹下创建一个anim文件夹专门存放我们的动画文件
anim_enter.xml如下
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="600"
android:fromAlpha="0"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:toAlpha="1" />
</set>
anim_pop_exit.xml如下
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="600"
android:fromAlpha="1"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:toAlpha="0" />
</set>
这两个动画就是设置透明度的,实现了淡入淡出的效果,很简单
然后改下之前的跳转标识,就不要直接用fragment的id去标识了,改成我们新连的action,现在运行跳转就有效果了
Navigation.findNavController(requireActivity(), R.id.fragment_container)
.navigate(R.id.action_homeFragment_to_picFragment)
之后给图片的显示也加上动画
这里我们继续用自定义的ScaleView
加载时我们也加上监听和之前adapter里的监听一样直接复制过来
运行一下发现有从小到大的效果了
但是突然的从小变大,在这里显得太突兀了,优化方案是修改scale的变化过程,改成从大概0.5到1而不是0到1,持续时间也相应的缩短一下
方案有了,接下来就是实施,直接从ScaleView开始起手
我们来声明两个成员变量一个是开始的比例,一个是持续时间,修饰符不加并且用var,这样可以暴露给使用者动态配置
之后就是 简单的修改一下draw方法和startAnim方法,看着不太好懂,但可以取几个值模拟一下可以得到类似的规律,写出来大概就是这样
private var num = Int.MAX_VALUE
var startScale = 0f //开始的比例
var duration = 400 //持续时间
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val count = duration / 10
if (num <= count) {
scaleX = num * 1f / count
scaleY = num * 1f / count
num++
invalidate()
}
}
fun startAnim() {
if (startScale > 1 || startScale < 0) startScale = 0f
num = (duration * startScale / 10).toInt()
}
}
接下来我们来适配静态配置
创建res/values/attrs.xml
代码如下
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="startScale" format="float"/>
<attr name="duration" format="integer"/>
<declare-styleable name="ScaleImage">
<attr name="startScale"/>
<attr name="duration"/>
</declare-styleable>
</resources>
接着在ScaleView的构造器中引用
constructor(context: Context?) : this(context, null)
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {
if (context != null) {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ScaleImage)
startScale = typedArray.getFloat(R.styleable.ScaleImage_startScale, 0f)
duration = typedArray.getInteger(R.styleable.ScaleImage_duration, 400)
typedArray.recycle()
}
}
这样就可以在布局文件中静态配置
添加如下两个字段
再次运行一下,现在就好多了
壁纸下载
首先加上下载按钮,默认是隐藏的
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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=".ui.PicItemFragment">
<com.zrq.nicepicture.view.ScaleImage
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="@drawable/pressed_background"
android:scaleType="centerCrop"
app:duration="200"
app:startScale="0.4" />
<RelativeLayout
android:id="@+id/relative_layout"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/transparent">
<TextView
android:id="@+id/btn_download"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="30dp"
android:background="@drawable/shape_btn_download"
android:foreground="@drawable/pressed_background"
android:text="下载到本地"
android:textColor="@color/black" />
</RelativeLayout>
</RelativeLayout>
pressed_background.xml文件如下
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<padding
android:bottom="10dp"
android:left="10dp"
android:right="10dp"
android:top="10dp" />
<solid android:color="#63FFFFFF" />
<stroke
android:width="1dp"
android:color="@color/grey" />
</shape>
在PicItemFragment方法里加上这几个事件监听
override fun initEvent() {
mBinding.apply {
image.setOnClickListener {
Navigation.findNavController(requireActivity(), R.id.fragment_container)
.popBackStack()
}
image.setOnLongClickListener {
relativeLayout.visibility = View.VISIBLE
true
}
relativeLayout.setOnClickListener {
relativeLayout.visibility = View.GONE
}
btnDownload.setOnClickListener {
btnDownload.text = "正在下载"
saveImage(requireContext(), url) { success, msg ->
Handler(Looper.getMainLooper()).post {
Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show()
if (success) {
btnDownload.text = "已下载"
btnDownload.isEnabled = false
relativeLayout.visibility = View.GONE
} else {
btnDownload.text = "下载失败"
}
}
}
}
}
}
下面是将url图片保存到本地的方法,我一开始没有申请读写权限发现也可以保存,所以就把权限那段删了,如果运行发现没有权限的话可以在AndroidManifest.xml里加上,然后再动态申请一下
fun saveImage(ctx: Context, url: String, callBack: (Boolean, String) -> Unit) {
var bitmap: Bitmap? = null
Thread {
var picUrl: URL? = null
try {
picUrl = URL(url)
} catch (e: Exception) {
e.printStackTrace()
}
if (picUrl != null) {
var inputStream: InputStream? = null
try {
val connect: HttpURLConnection = picUrl.openConnection() as HttpURLConnection
connect.doInput = true
connect.connect()
inputStream = connect.inputStream
bitmap = BitmapFactory.decodeStream(inputStream)
} catch (e: IOException) {
e.printStackTrace()
callBack(false, "图片保存失败: error4")
} finally {
inputStream?.close()
}
}
if (bitmap != null) {
val sdDir = ctx.getExternalFilesDir(null)
val filePath = sdDir!!.absolutePath + File.separator + "nice_pic"
Log.d(TAG, "saveImage: $filePath")
val appDir = File(filePath)
Log.d(TAG, "saveImage: ${appDir.exists()}")
if (!appDir.exists()) {
val mkdir = appDir.mkdir()
Log.d(TAG, "saveImage: $mkdir")
}
val time = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(Date())
val fileName = "LSP_$time.jpg"
val typeFor = URLConnection.getFileNameMap().getContentTypeFor(fileName)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val value = ContentValues()
value.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
value.put(MediaStore.MediaColumns.MIME_TYPE, typeFor)
value.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
val contentResolver = ctx.contentResolver
val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, value)
if (uri == null) {
callBack(false, "图片保存失败:error1")
return@Thread
}
var os: OutputStream? = null
try {
os = contentResolver.openOutputStream(uri)
val success = bitmap!!.compress(Bitmap.CompressFormat.JPEG, 100, os)
if (success) {
callBack(true, "图片保存成功")
ctx.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri))
} else {
callBack(false, "图片保存失败:error3")
}
} catch (e: IOException) {
callBack(false, "图片保存失败:error2")
} finally {
os?.flush()
os?.close()
}
} else {
MediaScannerConnection.scanFile(ctx, arrayOf(filePath), arrayOf(typeFor)) { _, _ ->
callBack(true, "图片保存成功")
}
}
}
}.start()
}
现在是可以下载了,但是后来调试发现使用preview这个字段下载下来的壁纸像素更高,这里我们把之前的改一下
首先PicFragment的这俩个位置改一下
adapter这里也改一下
把PicItemFragment的构造器改一下
class PicItemFragment(private val pic: Vertical)
现在下载的图片就是preview
运行如下
接下来在首页小图长按我们也加上下载功能
首先adapter里加上长按回调
在调用位置实现该方法
主要就是先获取到点击item的位置,用来定位下载按钮
picAdapter = PicAdapter(requireContext(), list,
{ _, pos ->
mainModel.list.clear()
mainModel.list.addAll(list)
mainModel.pos = pos
Navigation.findNavController(requireActivity(), R.id.fragment_container)
.navigate(R.id.action_homeFragment_to_picFragment)
},
{ view, position ->
val location = IntArray(2)
view.getLocationInWindow(location)
location[0] += view.width / 2
location[1] += view.height / 2
if (downloadBtn != null) {
mBinding.root.removeView(downloadBtn)
}
downloadBtn = newBtn(location[0], location[1])
downloadBtn?.let { btn ->
btn.setOnClickListener {
saveImage(requireContext(), list[position].preview) { success, msg ->
Handler(Looper.getMainLooper()).post {
Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show()
if (success) {
btn.text = "已下载"
btn.isEnabled = false
}
}
}
}
}
}
)
其中现在按钮如下,这里宽高可以自己配置,drawable我直接用的之前的
private var downloadBtn: Button? = null
@SuppressLint("UseCompatLoadingForDrawables")
private fun newBtn(x: Int, y: Int): Button {
val btn = MaterialButton(requireContext())
val width = 240
val height = 120
btn.layoutParams = RelativeLayout.LayoutParams(width, height)
btn.cornerRadius = 20
btn.background = resources.getDrawable(R.drawable.shape_btn_download)
btn.text = "下载"
btn.x = x.toFloat() - width / 2
btn.y = y.toFloat() - height / 2
mBinding.root.addView(btn)
return btn
}
当然在滑动时销毁该button
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
mBinding.root.removeView(downloadBtn)
}
})
现在运行一下
可以,现在大功告成了,可以愉快的看涩图了