? ? ? ? 在用ViewPager配合Fragment开发的模式中,想做一个类似于桌面壁纸的背景图,可以跟着ViewPager滑动。这里贴一下项目初期实现了的效果:
?可以看到,界面中ViewPager滑动的同时背景图片也跟着滑动了,二者的滑动速率不一样。那么来
?大体思路:
? ? ? ? ? ? ? ? 在ViewPager滑动的过程中,监听滑动百分比,再通过这个滑动的百分比来控制背景图的偏移,背景图的偏移通过背景图的尺寸和View容器的尺寸来计算。最后将这个偏移后的图片显示在ImageView或者某个View的Drawable上。(其实SurfaceView的性能会强得多,但是SurfaceView没有View属性,而且放在布局中还会让其他View的显示出现一些问题,特别是有半透明,阴影这些地方)
????????那么接下来就开始吧,首先准备好一张背景图片,一个ViewPager,作者用的是VIewPager2。
? ? ? ? 这里先放一个简单的一个ViewPager2。
ViewPagerAdapter.java
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.jetbrains.annotations.NotNull;
/**
* @author ldh
* 时间: 2021/10/23 13:51
* 邮箱: 2637614077@qq.com
*/
public class ViewPagerAdapter extends RecyclerView.Adapter<ViewPagerAdapter.MyViewHolder> {
int maxCount = 0;
public ViewPagerAdapter(int maxCount){
this.maxCount = maxCount;
}
@NonNull
@NotNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull @NotNull ViewGroup parent, int viewType) {
return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_viewpager, parent, false));
}
@Override
public void onBindViewHolder(@NonNull @NotNull MyViewHolder holder, int position) {
holder.textView.setText("这是第" + (position + 1) + "页");
}
@Override
public int getItemCount() {
return maxCount;
}
class MyViewHolder extends RecyclerView.ViewHolder {
TextView textView;
public MyViewHolder(@NonNull @NotNull View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.item_viewpager_textview);
}
}
}
MainActivity.kt
val viewpager2 = findViewById<ViewPager2>(R.id.viewpager2)
viewpager2.adapter = ViewPagerAdapter(4)
现在是这个效果,此时只是简单的完成了一个ViewPager。
?
?然后我们写一个工具类,用偏移比率来描述偏移量。
偏移比率:[0,1]之间,初始为0,0表示还没开始划,1就是已经划完了。因为图片的尺寸和view的尺寸是可能会随着需求动态变化的,所以只记录偏移的比例。
ImageScroller.kt
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.os.Looper
import android.util.Log
import android.widget.ImageView
import kotlin.math.roundToInt
/**
* @author ldh
* 时间: 2021/10/23 14:09
* 邮箱: 2637614077@qq.com
*/
class ImageScroller {
private var bitmap: Bitmap? = null
private val matrix = Matrix()
/**
* 分别是水平方向和竖直方向的偏移比率
*/
private var offsetRateVertical: Float = 0f
private var offsetRateHorizontal: Float = 0f
/**
* 关联的ImageView,如果ImageView为空,则设置View的Drawable,否则直接让ImageView显示图片
*/
private var imageView: ImageView? = null
/**
* 容器的尺寸,也就是view的宽和高
*/
private var containerHeight: Int = 0
private var containerWidth: Int = 0
/**
* 图片的尺寸(经过拉伸后的)
*/
private var imageHeight: Int = 0
private var imageWidth: Int = 0
companion object {
/**
* 滚动方向,默认为水平滚动
*/
const val SCROLL_MODE_VERTICAL = 0
const val SCROLL_MODE_HORIZONTAL = 1
}
private var scrollMode: Int = SCROLL_MODE_HORIZONTAL
/**
* 计算图片信息等相关参数
*/
private fun measure() {
bitmap?.let {
if (containerHeight != 0 && containerWidth != 0) {
//如果容器尺寸还没算,这里就没有意义了,所以加个判断
var scale = 0f;
if (scrollMode == SCROLL_MODE_HORIZONTAL) {
//水平滚动模式
//使图片的高度拉伸到图片的高度
//如果拉伸完过后图片的宽度比容器的宽度还小,那就使图片宽度拉伸到屏幕宽度,不然就会形成空白区域
scale = containerHeight / (it.height).toFloat()
val width = it.width * scale;
if (width < containerWidth) {
scale = containerWidth / (it.width).toFloat()
}
} else {
//竖直滚动模式,逻辑同理
scale = containerWidth / (it.width).toFloat()
val height = it.height * scale;
if (height < containerHeight) {
scale = containerHeight / (it.height).toFloat()
}
}
imageWidth = (it.width * scale).roundToInt()
imageHeight = (it.height * scale).roundToInt()
matrix.setScale(scale, scale)
matrix.postTranslate(
offsetRateHorizontal * (containerWidth - imageWidth),
offsetRateVertical * (containerHeight - imageHeight))
draw()
}
}
}
/**
* 设置图片,设置图片之后要计算参数
*/
fun setBitmap(bitmap: Bitmap){
this.bitmap = bitmap
imageView?.setImageBitmap(bitmap)
imageView?.scaleType = ImageView.ScaleType.MATRIX
measure()
}
fun isVerticalScrollMode() = (scrollMode == SCROLL_MODE_VERTICAL)
fun isHorizontalScrollMode() = (scrollMode == SCROLL_MODE_HORIZONTAL)
/**
* 计算容器的尺寸
* 计算完过后还要计算一遍measure()
*/
private fun measureContainer() {
imageView?.post {
containerHeight = imageView!!.height
containerWidth = imageView!!.width
measure()
}
}
fun setupWithImageView(imageView: ImageView){
if(this.imageView != null && this.imageView != imageView){
return
}
imageView.scaleType = ImageView.ScaleType.MATRIX
this.imageView = imageView
bitmap?.let {
setBitmap(it)
}
measureContainer()
}
fun setHorizontalScrollMode(){
setScrollMode(SCROLL_MODE_HORIZONTAL)
}
fun setVerticalScrollMode(){
setScrollMode(SCROLL_MODE_VERTICAL)
}
fun setScrollMode(scrollMode: Int){
//每次发生切换的时候要重新measure()一次
if (scrollMode == SCROLL_MODE_VERTICAL && this.scrollMode == SCROLL_MODE_HORIZONTAL){
//从水平滚动切换到竖直滚动
this.scrollMode = SCROLL_MODE_VERTICAL
measure()
}
if(this.scrollMode == SCROLL_MODE_VERTICAL && scrollMode == SCROLL_MODE_HORIZONTAL){
//从竖直滚动切换到滚动水平
this.scrollMode = SCROLL_MODE_HORIZONTAL
measure()
}
}
/**
* 增加或减少偏移率Y
*/
fun varOffsetVerticalRate(offsetY: Float){
this.offsetRateVertical += offsetY;
matrix.postTranslate(0f, (containerHeight - imageHeight) * offsetY)
draw()
}
fun setOffsetRate(offsetRate: Float) {
//如果直接用set,那scale属性就没了
matrix.postTranslate(-(containerWidth - imageWidth) * offsetRateHorizontal, -(containerHeight - imageHeight) * offsetRateVertical)
if(isVerticalScrollMode()){
offsetRateVertical = offsetRate
}else {
offsetRateHorizontal = offsetRate
}
matrix.postTranslate((containerWidth - imageWidth) * offsetRateHorizontal, (containerHeight - imageHeight) * offsetRateVertical)
draw()
}
/**
* 增加或减少偏移率X
*/
fun varOffsetHorizontalRate(offsetX: Float){
this.offsetRateHorizontal += offsetX;
matrix.postTranslate( (containerWidth - imageWidth) * offsetX, 0f)
draw()
}
fun draw(){
//因为不能在子线程更新UI,所以每次要判断当前是否在主线程中
if(isMainThread()){
if(imageView != null){
imageView!!.setImageMatrix(matrix)
}
}else{
//如果是子线程调用的,那就发送到主线程去
imageView?.post {
draw()
}
}
}
/**
* 判断当前程序是否在主线程中运行
*/
fun isMainThread(): Boolean {
return Thread.currentThread() === Looper.getMainLooper().thread
}
constructor()
constructor(imageView: ImageView){
setupWithImageView(imageView)
}
constructor(imageView: ImageView,bitmap: Bitmap){
setupWithImageView(imageView)
setBitmap(bitmap)
}
constructor(imageView: ImageView, id: Int){
setupWithImageView(imageView)
setBitmap(BitmapFactory.decodeResource(imageView.context.resources, id))
}
}
?再写一个工具,用来绑定ViewPager和ImageScroller,作者在这里只写了一个ViewPager2的绑定器,其他的可以自己写,这里通过判断ViewPager2是竖直滑动还是水平滑动来动态改变ImageScroller的滑动模式
BindUtils.kt
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import java.util.concurrent.Executors
/**
* @author ldh
* 时间: 2021/10/23 18:25
* 邮箱: 2637614077@qq.com
*/
object BindUtils {
fun bindWithViewPager2(viewPager2: ViewPager2?, imageScroller: ImageScroller?) {
if(viewPager2 != null && imageScroller != null) {
if(viewPager2.orientation == RecyclerView.HORIZONTAL){
//viewPager是水平滑动,那设置ImageScroller也是水平滑动
imageScroller.setHorizontalScrollMode()
}else {
imageScroller.setVerticalScrollMode()
}
viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
//这里用子线程控制Image滑动,如果用主线程的话容易造成滑动动画卡顿掉帧
val executorService = Executors.newSingleThreadExecutor();
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
executorService.execute {
imageScroller.setOffsetRate((position + positionOffset) / ((viewPager2.adapter as RecyclerView.Adapter).itemCount - 1))
}
}
})
}
}
}
最后再贴一下MainActivity的代码
MainActivity.kt
import android.os.Bundle
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.viewpager2.widget.ViewPager2
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportActionBar?.hide()
val viewpager1 = findViewById<ViewPager2>(R.id.viewpager1)
viewpager1.adapter = ViewPagerAdapter(4)
val imageScroller1 = ImageScroller(findViewById<ImageView>(R.id.imageView1), R.drawable.color_clouds)
BindUtils.bindWithViewPager2(viewpager1, imageScroller1)
val viewpager2 = findViewById<ViewPager2>(R.id.viewpager2)
viewpager2.adapter = ViewPagerAdapter(4)
val imageScroller2 = ImageScroller(findViewById<ImageView>(R.id.imageView2), R.drawable.image_long)
BindUtils.bindWithViewPager2(viewpager2, imageScroller2)
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<RelativeLayout
android:layout_marginHorizontal="80dp"
android:layout_marginVertical="10dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/imageView1"/>
<androidx.viewpager2.widget.ViewPager2
android:orientation="horizontal"
android:id="@+id/viewpager1"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</RelativeLayout>
<RelativeLayout
android:layout_marginHorizontal="80dp"
android:layout_marginVertical="10dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/imageView2"/>
<androidx.viewpager2.widget.ViewPager2
android:orientation="vertical"
android:id="@+id/viewpager2"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</RelativeLayout>
</LinearLayout>
实现出来的效果:
?原理其实很简单,就是监听ViewPager2的滑动,根据滑动百分比来设置图片的偏移。核心代码就两个文件:ImageScroller.kt和BindUtils.kt,demo我已经上传到码云上了。
本项目地址:ImageScroller
|