有趣的ui效果——随列表移动的小飞机

前言

这篇文章的起因是,群里一个小伙伴去面试时被问到一个效果如何实现:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7POiiFUz-1586686264261)(https://wanandroid.com/blogimgs/81c87faf-49e4-4041-80c8-9e188d1390c4.gif)]

后来鸿洋看到后,就把这个效果放到wanandroid网站上作为一个问题。

有趣的效果 小船儿游而游

然后我用闲暇时间对这个效果做了个简单的实现,本文就是介绍这个实现的思路的。

从图中的效果可以看出,小船是沿着固定的线路在移动,他的移动随着item的移动而移动的,所以我们先实现一个能够沿着固定路径移动的自定义控件,然后把这个控件作为RecyclerView的item就好。

PathView的实现

我们先看PathView的实现。

新建一个类继承View,重写起的onDraw方法,然后向外暴露传入图片以及Path路径的方法:

public class PathView extends View {
   
  ···
    public void setPath(Path path) {
        mPath = path;
    }


    public void setImage(int imageRes) {
        mBitmap = BitmapFactory.decodeResource(getResources(), imageRes);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(mPath, mPaint);
      ···
    }


}

现在问题的关键是,如何通过外界传入一个进度值,将图片画到对应的路径的位置,并将其旋转到位。

这就要借助一个类:PathMeasure,通过其来确定图片的位置以及旋转的角度。

mPathMeasure = new PathMeasure(mPath, false);
mLength = mPathMeasure.getLength();

mPathMeasure.getPosTan(progress / mMax * mLength, pos, tan);

通过如上代码可以获取到path路径在指定百分比位置的坐标以及切线方向。其中pos的两个值分别为位置坐标,而tan的两个值为该点对应切线方向的两个坐标。

扫描二维码关注公众号,回复: 10780439 查看本文章
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawPath(mPath, mPaint);
    if (mProgress >= 0) {
        canvas.save();
        canvas.translate(pos[0], pos[1]);
        double atan = Math.atan(tan[1] / tan[0]);
        float degrees = (float) (atan * 180 / Math.PI);
        Log.e(TAG, "degrees " + degrees);
        degrees += 90;//我选取的图片是朝上的,而Android的极坐标系是朝右的,朝上的角度为-90度
        if (tan[0] < 0) {
            degrees += 180;
        }
        canvas.rotate(degrees);
        canvas.drawBitmap(mBitmap, null, mRect, mPaint);
        canvas.restore();
    }
}

所以画具体的图片的方法如上,其中在切线的x分量小于0时要多旋转180度,是图片的朝向是向着左边,也就是x<0的方向。

PathView的完整代码如下:

public class PathView extends View {
    public static final String TAG = "PathView";

    private Path mPath;
    private Paint mPaint;
    private int mProgress = -1;
    private Bitmap mBitmap;
    private Rect mRect;
    private float[] pos = new float[2];
    private float[] tan = new float[2];
    private PathMeasure mPathMeasure;
    private float mLength;
    private float mMax = 100f;

    public PathView(Context context) {
        this(context, null);
    }

    public PathView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PathView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
    }

    private void initPaint() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(0xff123456);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(40);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
    }

    public void setPath(Path path) {
        mPath = path;
        mPathMeasure = new PathMeasure(mPath, false);
        mLength = mPathMeasure.getLength();
        postInvalidate();
    }

    public void setMax(int max) {
        mMax = max;
    }

    public void setProgress(int progress) {
        mProgress = progress;
        if (mPath != null) {
            mPathMeasure.getPosTan(progress / mMax * mLength, pos, tan);
            Log.e(TAG, "progress " + progress + " pos " + pos[0] + " " + pos[1] + " " + "tan " + tan[0] + " " + tan[1]);


        }
//        postInvalidate();
        invalidate();
    }

    public void setImage(int imageRes) {
        mBitmap = BitmapFactory.decodeResource(getResources(), imageRes);
        int width = mBitmap.getWidth();
        int height = mBitmap.getHeight();
        mRect = new Rect(-width / 2, -height / 2, width / 2, height / 2);
        postInvalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(mPath, mPaint);
        if (mProgress >= 0) {
            canvas.save();
            canvas.translate(pos[0], pos[1]);
            double atan = Math.atan(tan[1] / tan[0]);
            float degrees = (float) (atan * 180 / Math.PI);
            Log.e(TAG, "pre degrees " + degrees);
            degrees += 90;
            if (tan[0] < 0) {
                degrees = degrees + 180;
            }
            Log.e(TAG, "last degrees " + degrees);

            canvas.rotate(degrees);
            if (mBitmap != null) {
                canvas.drawBitmap(mBitmap, null, mRect, mPaint);
            }
            canvas.restore();
        }
    }


}

可以用如下代码测试这个控件:

final PathView pathView = findViewById(R.id.path_view);
Path path = new Path();
path.addCircle(500, 800, 400, Path.Direction.CCW);
pathView.setImage(R.mipmap.airplane2);
pathView.setPath(path);
final Handler handler = new Handler();
handler.post(new Runnable() {
    private int i = 0;
    @Override
    public void run() {
        pathView.setProgress(i++ % 100);
        handler.postDelayed(this,50);
    }
});

在这里插入图片描述

第一种实现方式

有了如上控件之后,实现目标效果就相对简单了。

先用第一种方式,每个item都放一个PathView,适配器代码如下:

public class PathAdapter extends RecyclerView.Adapter<PathAdapter.PathViewHolder> {
    private Context mContext;
    private Path mLeftPath;
    private Path mRightPath;

    public PathAdapter(Context context, int width) {
        mContext = context;
        initLeftPath(width);
        initRightPath(width);
    }

    @NonNull
    @Override
    public PathViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        PathView pathView = new PathView(mContext);
        pathView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 300));
        if (viewType == 0) {
            pathView.setPath(mLeftPath);
            pathView.setBackgroundColor(0xff134334);
        } else {
            pathView.setPath(mRightPath);
            pathView.setBackgroundColor(0xff752397);
        }
        pathView.setImage(R.mipmap.airplane2);
        return new PathViewHolder(pathView);
    }

    private void initLeftPath(int width) {
        mLeftPath = new Path();
        mLeftPath.moveTo(width / 3, 0);
        mLeftPath.lineTo(width / 3, 250);
        mLeftPath.lineTo(width / 3 * 2, 250);
        mLeftPath.lineTo(width / 3 * 2, 300);
    }

    private void initRightPath(int width) {
        mRightPath = new Path();
        mRightPath.moveTo(width / 3 * 2, 0);
        mRightPath.lineTo(width / 3 * 2, 250);
        mRightPath.lineTo(width / 3, 250);
        mRightPath.lineTo(width / 3, 300);
    }

    @Override
    public void onBindViewHolder(@NonNull PathViewHolder holder, int position) {

    }

    @Override
    public int getItemViewType(int position) {
        return position % 2;
    }

    @Override
    public int getItemCount() {
        return 100;
    }

    class PathViewHolder extends RecyclerView.ViewHolder {

        public PathViewHolder(@NonNull View itemView) {
            super(itemView);
        }
    }
}

逻辑很简单,根据奇偶位置来放置两种item就好。

然后最关键的,让小飞机随着RecyclerView的滑动而改变位置,并且只有一个item显示出小飞机,实现如下:

recyclerView.setAdapter(new PathAdapter(this, (int) (screenWidthDp * scale + 0.5f)));

recyclerView.setClipChildren(false);

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        Log.e(TAG, "" + dy);
        int childCount = recyclerView.getChildCount();
        for (int i = 0; i < childCount; i++) {
            PathView child = (PathView) recyclerView.getChildAt(i);
            float line = (getResources().getConfiguration().screenHeightDp * scale + 0.5f) / 3;
            Log.e(TAG, "i " + i + " child.getTop() " + child.getTop());
            if (child.getTop() > line) {
                int progress = (int) ((child.getTop() - line) / child.getHeight() * 100);
                Log.e(TAG, "progress " + progress);
                if (progress < 0 || progress > 100) {
                    Log.e(TAG, "onScrolled: error progress " + progress);
                }
                if (child != currentPathView) {
                    if (currentPathView != null) {
                        child.setProgress(100 - progress);

                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                            currentPathView.setZ(0);
                        }
                        currentPathView.setProgress(-1);
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                            child.setZ(1);
                        }

                    } else {
                        child.setProgress(100 - progress);
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                            child.setZ(1);
                        }
                    }
                    currentPathView = child;
                } else {
                    child.setProgress(100 - progress);
                }
                return;
            }
        }

    }
});

思路是,找到第一个top在某个基线之下的item,然后让他的飞机显示出来,进度的计算就是top与基线的距离与item高度的比值。其中对z的设置是为了让有飞机的item在其他item上面,从而实现飞机的顺畅显示。

最终的效果如下:

第一种效果图

第二种实现方式

第一种实现有个弊端,就是飞机显示出来的只有一个,却在很多个item有了实例,现在用另一种方式来实现,只用一个PathView。

先初始化一个PathView:

mPathView = findViewById(R.id.path_view);
path = new Path();
path.moveTo(screenWidth / 3, 0);
for (int i = 0; i < 50; i++) {
    path.lineTo(screenWidth / 3, 250 + 300 * i * 2);
    path.lineTo(screenWidth / 3 * 2, 250 + 300 * i * 2);
    path.lineTo(screenWidth / 3 * 2, 300 + 300 * i * 2);

    path.lineTo(screenWidth / 3 * 2, 250 + 300 * (i * 2 + 1));
    path.lineTo(screenWidth / 3, 250 + 300 * (i * 2 + 1));
    path.lineTo(screenWidth / 3, 300 + 300 * (i * 2 + 1));

}
mPathView.setPath(path);
final int max = 10000;
mPathView.setMax(max);

然后在进度改变的时候,移动path,并改变progress:

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    int scrollY = 0;

    long time = System.currentTimeMillis();

    @Override
    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        path.offset(dx, -dy);
        mPathView.setPath(path);
        scrollY += dy;
        int progress = (int) (scrollY / (300 * 100 - screenHeightDp * scale) * max);
        mPathView.setProgress(progress);
        Log.e(TAG, "onScrolled: scrollY " + scrollY);
        Log.e(TAG, "onScrolled: progress " + progress);
        Log.e(TAG, "onScrolled: progress " + progress);
        long currentTimeMillis = System.currentTimeMillis();
        int dtime = (int) (currentTimeMillis - time);
        Log.e(TAG, "onScrolled: dtime " + dtime + " dy " + dy);

        mPathView.setAnimationSpeed(dy / dtime);
        time = currentTimeMillis;


    }
});

最终实现效果如下:

在这里插入图片描述

其中还将小船添加了进去。

完整e的代码参考github小船

发布了19 篇原创文章 · 获赞 8 · 访问量 4037

猜你喜欢

转载自blog.csdn.net/u014068277/article/details/105473427