准备工作
首先导包
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: Tprotected lateinit var mainModel: MainModeloverride 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?): Tabstract 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.FragmentContainerViewandroid: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.SmartRefreshLayoutandroid:id="@+id/refresh_layout"android:layout_width="match_parent"android:layout_height="match_parent"><com.scwang.smart.refresh.header.ClassicsHeaderandroid:layout_width="match_parent"android:layout_height="wrap_content" /><androidx.recyclerview.widget.RecyclerViewandroid: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.ClassicsFooterandroid: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.ShapeableImageViewandroid: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 = 0loadPic()}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.sizepicture?.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.headerstry {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.windowwindow.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREENor 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.windowwindow.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 = 0if (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_BARresult = 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 = falseif (window != null) {try {val lp = window.attributesval darkFlag = WindowManager.LayoutParams::class.java.getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON")val meizuFlags = WindowManager.LayoutParams::class.java.getDeclaredField("meizuFlags")darkFlag.isAccessible = truemeizuFlags.isAccessible = trueval 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 = lpresult = true} catch (e: Exception) {}}return result}/*** 需要MIUIV6以上** @param activity* @param dark 是否把状态栏文字及图标颜色设置为深色* @return boolean 成功执行返回true*/fun MIUISetStatusBarLightMode(activity: Activity, dark: Boolean): Boolean {var result = falseval window = activity.windowif (window != null) {val clazz: Class<*> = window.javaClasstry {var darkModeFlag = 0val 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 = trueif (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_VALUEprivate val count = 40foverride fun onDraw(canvas: Canvas?) {super.onDraw(canvas)if (num <= count) {scaleX = num / countscaleY = num / countnum++}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.ScaleImageandroid: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 = posNavigation.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.ShapeableImageViewandroid: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: PicItemAdapterprivate 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"><alphaandroid: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"><alphaandroid: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_VALUEvar startScale = 0f //开始的比例var duration = 400 //持续时间override fun onDraw(canvas: Canvas?) {super.onDraw(canvas)val count = duration / 10if (num <= count) {scaleX = num * 1f / countscaleY = num * 1f / countnum++invalidate()}}fun startAnim() {if (startScale > 1 || startScale < 0) startScale = 0fnum = (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.ScaleImageandroid: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" /><RelativeLayoutandroid:id="@+id/relative_layout"android:visibility="gone"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/transparent"><TextViewandroid: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" /><paddingandroid:bottom="10dp"android:left="10dp"android:right="10dp"android:top="10dp" /><solid android:color="#63FFFFFF" /><strokeandroid: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.VISIBLEtrue}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 = falserelativeLayout.visibility = View.GONE} else {btnDownload.text = "下载失败"}}}}}}
下面是将url图片保存到本地的方法,我一开始没有申请读写权限发现也可以保存,所以就把权限那段删了,如果运行发现没有权限的话可以在AndroidManifest.xml里加上,然后再动态申请一下
fun saveImage(ctx: Context, url: String, callBack: (Boolean, String) -> Unit) {var bitmap: Bitmap? = nullThread {var picUrl: URL? = nulltry {picUrl = URL(url)} catch (e: Exception) {e.printStackTrace()}if (picUrl != null) {var inputStream: InputStream? = nulltry {val connect: HttpURLConnection = picUrl.openConnection() as HttpURLConnectionconnect.doInput = trueconnect.connect()inputStream = connect.inputStreambitmap = 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.contentResolverval uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, value)if (uri == null) {callBack(false, "图片保存失败:error1")return@Thread}var os: OutputStream? = nulltry {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 = posNavigation.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 / 2location[1] += view.height / 2if (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 = 240val height = 120btn.layoutParams = RelativeLayout.LayoutParams(width, height)btn.cornerRadius = 20btn.background = resources.getDrawable(R.drawable.shape_btn_download)btn.text = "下载"btn.x = x.toFloat() - width / 2btn.y = y.toFloat() - height / 2mBinding.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)}
})
现在运行一下
可以,现在大功告成了,可以愉快的看涩图了
项目地址:CanCanWorld/NicePic (github.com)