先看一下效果:
其中顶部是模仿的股票数据分时图,以前也写过详细的文章传送门,只不过不支持左右滑动,这款是在那个基础上的修改
在说一下分时图的思路吧:
其中滑动的核心代码如下:
mShowList 为需要显示的集合
mTimeList 为全部数据集合
通过 subList 截取需要的数据段,截取后实时去刷新当前View
/**
* 滑动监听
* */
//当前的X轴坐标,我要记录上次滑动的最后坐标,才能实时判断是否左右滑动
private float startTouchX = 0f;
//滑动的距离,通过他来截取对应的数据段
private int moveTouchX = 0;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//手指按下
Log.e(TAG, "onTouchEvent: 123456789->按下");
//moveTouchX = 0;
startTouchX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
if (mTimeList.size() > 50){
stopRe = true;
//手指移动
if (startTouchX > event.getX()){
Log.e(TAG, "onTouchEvent: 123456789->向左滑动--" + moveTouchX);
if (moveTouchX > 0){
moveTouchX = moveTouchX - 1;
mShowList.clear();
mShowList.addAll(mTimeList.subList(mTimeList.size() - (50 + moveTouchX), mTimeList.size() - moveTouchX));
postInvalidate();
}else {
moveTouchX = 0;
stopRe = false;
}
}else {
Log.e(TAG, "onTouchEvent: 123456789->向右滑动---" + moveTouchX);
if (mTimeList.size() > (50 + moveTouchX)){
moveTouchX = moveTouchX + 1;
mShowList.clear();
mShowList.addAll(mTimeList.subList(mTimeList.size() - (50 + moveTouchX), mTimeList.size() - moveTouchX));
}
postInvalidate();
}
}
startTouchX = event.getX();
break;
case MotionEvent.ACTION_UP:
//手指松开
Log.e(TAG, "onTouchEvent: 123456789->松开");
break;
}
return true;
}
下方三个动画(属性动画)也是我遇到过比较常见的
第一个是放大缩小,比如在送礼按钮上,和礼物图标上,更有冲击感
第二个是上下移动,比如某个按钮上的气泡
第三个是卡片点击左右两边时有一个缓冲效果
当然常见的还有渐变效果,比如占位图消失时,加一个渐变消失用户体验可能会更好
下面贴一下代码
可滑动分时图: TimeView
/**
* 作者:zch
* 时间:2023/10/10 15:08
* 描述:自定义分时图View
*/
public class TimeView extends View {
private Paint paint;
private int mCanvasHeight;
private int mCanvasWidth;
private int mStartX = 0;
private int mStartY;
private int paintColor = 0;
private int canvasColor = 0;
private int bgColor = 0;
private boolean isShowBg = false;
//是否停止刷新,但是总数据一样往里面添加
private boolean stopRe = false;
//总数据
private ArrayList<Integer> mTimeList = new ArrayList<>();
//需要显示在屏幕上的数据
private ArrayList<Integer> mShowList = new ArrayList<>();
public TimeView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
//开始绘画逻辑
@SuppressLint("DrawAllocation")
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//首先定义画笔
paint = new Paint();
//防锯齿
paint.setAntiAlias(true);
//获取画板高度
mCanvasHeight = getMeasuredHeight();
//获取画板宽度
mCanvasWidth = getMeasuredWidth();
//设置画笔颜色
paint.setColor(paintColor == 0 ? Color.YELLOW : paintColor);
//设置画板背景
canvas.drawColor(canvasColor == 0 ? Color.GRAY : canvasColor);
//设置画笔粗细
paint.setStrokeWidth((float)3);
//Y轴的起止都在折线图的四分之一处开始,因为数据的范围是折线图的二分之一
//这样数据波动位置就处于折线图居中位置
mStartY = mCanvasHeight / 4;
//画线
drawView(canvas);
}
/**
*注释:
*绘画逻辑
*/
private void drawView(Canvas canvas){
mStartX = 0;
//如果有数据
if (mShowList.size() > 0){
int startX = 0,startY = 0;
int stopX,stopY;
for (int i = 0; i < mShowList.size(); i++){
Integer t = mShowList.get(i);
stopY = t+ mStartY;
mStartX = mStartX + 10;
//stopX = t.getStopX();
stopX = mStartX;
//首次的时候起始位置就等于终止位置
if (i == 0){
startX = stopX;
startY = stopY;
}
//画线
canvas.drawLine(startX,startY,stopX,stopY,paint);
//是否画背景
if (isShowBg){
setDrawBg(startX,startY,stopX,stopY,canvas);
}
//下次画线的时候的起始位置等于上次画线位置的终止位置
startX = stopX;
startY = stopY;
}
}
}
/**
* 滑动监听
* */
//当前的X轴坐标,我要记录上次滑动的最后坐标,才能实时判断是否左右滑动
private float startTouchX = 0f;
//滑动的距离,通过他来截取对应的数据段
private int moveTouchX = 0;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//手指按下
Log.e(TAG, "onTouchEvent: 123456789->按下");
//moveTouchX = 0;
startTouchX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
if (mTimeList.size() > 50){
stopRe = true;
//手指移动
if (startTouchX > event.getX()){
Log.e(TAG, "onTouchEvent: 123456789->向左滑动--" + moveTouchX);
if (moveTouchX > 0){
moveTouchX = moveTouchX - 1;
mShowList.clear();
mShowList.addAll(mTimeList.subList(mTimeList.size() - (50 + moveTouchX), mTimeList.size() - moveTouchX));
postInvalidate();
}else {
moveTouchX = 0;
stopRe = false;
}
}else {
Log.e(TAG, "onTouchEvent: 123456789->向右滑动---" + moveTouchX);
if (mTimeList.size() > (50 + moveTouchX)){
moveTouchX = moveTouchX + 1;
mShowList.clear();
mShowList.addAll(mTimeList.subList(mTimeList.size() - (50 + moveTouchX), mTimeList.size() - moveTouchX));
}
postInvalidate();
}
}
startTouchX = event.getX();
break;
case MotionEvent.ACTION_UP:
//手指松开
Log.e(TAG, "onTouchEvent: 123456789->松开");
break;
}
return true;
}
/**
*注释:
* 画折线下背景
*/
private void setDrawBg(int tx,int ty,int px,int py,Canvas canvas){
//设置画笔
Paint paint = new Paint();
//防锯齿
paint.setAntiAlias(true);
//设置颜色
paint.setColor(bgColor == 0 ? Color.WHITE : bgColor);
//画阴影部分
Path bg = new Path();
bg.moveTo(tx, ty);
bg.lineTo(tx, mCanvasHeight);
bg.lineTo(px, mCanvasHeight);
bg.lineTo(px, py);
bg.lineTo(tx, ty);
bg.close();
//添加到画板上
canvas.drawPath(bg, paint);
}
/**
*注释:
* 设置分数图数据,由外部传输
*/
public void setViewData(ArrayList<Integer> m) {
this.mTimeList = m;
//刷新界面 - 无需在UI线程,在工作线程即可被调用,invalidate()必须在UI线程
if (!stopRe){
mShowList.clear();
//只取最后50个数据
if (mTimeList.size() > 50){
mShowList.addAll(mTimeList.subList(mTimeList.size()-50, mTimeList.size() - 1));
}else {
mShowList.addAll(mTimeList);
}
postInvalidate();
}
}
/**
*注释:
*设置分时图背景和画笔颜色
* p - 画笔颜色
* c - 背景颜色
* b - 折线下背景颜色
*/
public void setViewColor(int p,int c,int b){
this.paintColor = p;
this.canvasColor = c;
this.bgColor = b;
}
/**
*注释:
* 是否显示折线下的背景
*/
public void setShowBgBottom(boolean b){
this.isShowBg = b;
}
}
使用:
这里用定时来动态获取数据,其中 initData 为请求数据,这里用的模拟数据
/**
* 通过线程来动态设置View达到动画效果
*/
private val thread: Thread = object : Thread() {
@RequiresApi(api = Build.VERSION_CODES.N)
override fun run() {
while (true) {
try {
initData()
//如果数据终止X的值大于界面宽度停止线程或者其他操作
/*if (mTimeList.size > 0 && mTimeList.get(mTimeList.size - 1).stopX > tvTime!!.measuredWidth) {
//清除容器数据,重新添加
startX = 0
mTimeList.clear()
//thread.stop(); //终止线程
}*/
/**休息时间 */
sleep(1000)
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
}
}
private val mTimeList: ArrayList<Int> = ArrayList()
@RequiresApi(Build.VERSION_CODES.N)
private fun initData() {
//设置Y轴数据的终止位置,无需设置起始位置,因为每条线的起始位置就是上一条线的终点位置
val ra = Random()
val stopY: Int = ra.nextInt(40)
//给分时图设置数据
mTimeList.add(stopY)
tvTime?.setViewData(mTimeList)
}
缩放动画:
这里分两部分,先缩,监听缩结束后去放,如此反复
//缩,将View传进来即可
fun showS1(view: View) {
val scaleAnimation = ScaleAnimation(
1f, 0.5f,
1f, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f
)
scaleAnimation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {}
override fun onAnimationEnd(animation: Animation) {
showF1(view)
}
override fun onAnimationRepeat(animation: Animation) {}
})
scaleAnimation.duration = 300
scaleAnimation.fillAfter = true
view.startAnimation(scaleAnimation)
}
//放
fun showF1(view: View) {
val scaleAnimation = ScaleAnimation(
0.5f, 1f,
0.5f, 1f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f
)
scaleAnimation.duration = 300
scaleAnimation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {}
override fun onAnimationEnd(animation: Animation) {
view.clearAnimation()
showS1(view)
}
override fun onAnimationRepeat(animation: Animation) {}
})
scaleAnimation.fillAfter = true
view.startAnimation(scaleAnimation)
}
上下动画
fun sx(){
val view = findViewById<TextView>(R.id.tv_vip1)
val upAnim = ObjectAnimator.ofFloat(view, "translationY", 0F, -50F, 0F)
upAnim.duration = 2000
upAnim.interpolator = LinearInterpolator()
upAnim.addListener(object :AnimatorListenerAdapter(){
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
//动画结束
}
})
upAnim.repeatCount = ValueAnimator.INFINITE;//无限循环
//启动
upAnim.start()
}
卡片倾斜动画
/**
* 倾斜动画 f- 倾斜角度,这里建议 30-50*/
fun showQX(view: View, f: Float?) {
val valueAnimator = ValueAnimator.ofFloat(0f, f!!)
valueAnimator.addUpdateListener { animation ->
val deltaY = animation.animatedValue as Float
view.rotationY = deltaY
}
valueAnimator.addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animator: Animator) {}
override fun onAnimationEnd(animator: Animator) {
showHuiFu(view, f)
}
override fun onAnimationCancel(animator: Animator) {}
override fun onAnimationRepeat(animator: Animator) {}
})
//默认duration是300毫秒
valueAnimator.duration = 300
valueAnimator.start()
}
/**
* 恢复倾斜动画 */
fun showHuiFu(view: View, f: Float?) {
val valueAnimator = ValueAnimator.ofFloat(f!!, 0f)
valueAnimator.addUpdateListener { animation ->
val deltaY = animation.animatedValue as Float
view.rotationY = deltaY
}
//默认duration是300毫秒
valueAnimator.duration = 300
valueAnimator.start()
}