Android炫酷ui 带你做一个背景跟着滚动的工具

Posted 5年程序员转换摆地摊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android炫酷ui 带你做一个背景跟着滚动的工具相关的知识,希望对你有一定的参考价值。

        在用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

以上是关于Android炫酷ui 带你做一个背景跟着滚动的工具的主要内容,如果未能解决你的问题,请参考以下文章

Android开源实战:手把手带你实现一个简单好用的搜索框(含历史搜索记录)

Android带你解析ScrollView--仿QQ空间标题栏渐变

数据挖掘实战:带你做客户价值分析(附代码)

平常收藏的酷炫的Android开源特效库

教你做炫酷的碎片式图片切换 (canvas)

手把手教你做一个python+matplotlib的炫酷的数据可视化动图