先看效果,如下图:
一。做一个效果之前,先分析由几种图形组成
1.画横、纵坐标和刻度
2.画柱形图
3.手势的监听和页面打开时自动滑动
二, 界面实现,分析各个组件和效果的实现
1.画x,y轴和坐标
画x轴,首先先计算view的总高度,减去上面的空白高度除于分数,就可以得出,x轴各个数字所在的位置,代码如下
float textHeight = mHeight + paddingTop - bottomHeight;//横坐标高度
vertialInterval = (textHeight - 200f) / 5;
canvas.drawLine(startChart,0,startChart,textHeight,mLinePaint);
canvas.drawLine(startChart,textHeight,mWidth,textHeight,mLinePaint);
for (int i=0; i < vertileText.length; i++){
float y = (vertialInterval*i);
mVertileTextpaint.getTextBounds(vertileText[i],0,vertileText[i].length(),mBound);
canvas.drawText(vertileText[i],0,textHeight - y + mBound.height() / 2,mVertileTextpaint);
if (i > 0)
canvas.drawLine(startChart,textHeight - y,startChart + 10,textHeight - y,mVertileTextpaint);
}
画Y轴,后面的柱形图的坐标用前面的坐标不断叠加,这里要控制滑动的区域,所以要剪裁canvas,要排除纵标左方的区域,这里裁剪使用
canvas.clipRect(startChart + 10, 0, mWidth, getHeight(), Region.Op.REPLACE);
前面表示裁剪的left,top,后面参数表示right,bottom坐标 ,接下来就可以画柱形图,和x轴刻度了
float chartTempStart = startChart + xInterVal;
float maxY = textHeight - 200f;
for (int i=0; i < horText.length; i++){
float height = 200f + (10f - barDatas.get(i).getNum()) / 10f * maxY;
String num = String.valueOf(barDatas.get(i).getNum());
mVertileTextpaint.getTextBounds(num,0,num.length(),mBound);
canvas.drawText(num,chartTempStart + mCurrentOrigin.x,height - mBound.height(),mVertileTextpaint);
RectF rectF = new RectF();
rectF.left = chartTempStart + mCurrentOrigin.x;
rectF.top = height;
rectF.right = chartTempStart + yellowBarWidth + mCurrentOrigin.x;
rectF.bottom = textHeight - 3;
canvas.drawRect(rectF,mChartPaint);
chartTempStart += ( barWidth + startChart);
}
float grayBarStart = startChart + xInterVal ;
for (int i=0; i < horText.length; i++){
float height = 200f + (10f - avgDatas.get(i).getNum()) / 10f * maxY;
RectF rectF = new RectF();
rectF.left = grayBarStart + (yellowBarWidth + barInterval) + mCurrentOrigin.x;
rectF.top = height;
rectF.right = grayBarStart + (yellowBarWidth + barInterval) + grayBarWidth + mCurrentOrigin.x;
rectF.bottom = textHeight - 3;
canvas.drawRect(rectF,mGrayPaint);
grayBarStart += (barWidth + startChart );
}
float textTempStart = textStart;
for (int i=0; i < horText.length; i++){
mVertileTextpaint.getTextBounds(horText[i],0,horText[i].length(),mBound);
canvas.drawText(horText[i],textTempStart - mBound.width()/2 + mCurrentOrigin.x,mHeight - bottomHeight + mBound.height() + 38f,mVertileTextpaint);
textTempStart += (bottomHeight + startChart);
}
三。接下是手势的处理,这是这个view主要的学习点
创建手势对象和监听器
mGestureDetector = new GestureDetectorCompat(context, mGestureListener);
private final GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {
//手指按下
@Override
public boolean onDown(MotionEvent e) {
// goToNearestBar();
return true;
}
//有效的滑动
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
switch (mCurrentScrollDirection) {
case NONE:
// 只允许在一个方向上滑动
if (Math.abs(distanceX) > Math.abs(distanceY)) {
if (distanceX > 0) {
mCurrentScrollDirection = Direction.LEFT;
} else {
mCurrentScrollDirection = RIGHT;
}
} else {
mCurrentScrollDirection = Direction.VERTICAL;
}
break;
case LEFT:
// Change direction if there was enough change.
if (Math.abs(distanceX) > Math.abs(distanceY) && (distanceX < 0)) {
mCurrentScrollDirection = RIGHT;
}
break;
case RIGHT:
// Change direction if there was enough change.
if (Math.abs(distanceX) > Math.abs(distanceY) && (distanceX > 0)) {
mCurrentScrollDirection = Direction.LEFT;
}
break;
}
// 重新计算滑动后的起点
switch (mCurrentScrollDirection) {
case LEFT:
case RIGHT:
mCurrentOrigin.x -= distanceX * mXScrollingSpeed;
ViewCompat.postInvalidateOnAnimation(CommentBar.this);
break;
}
Log.e(" mCurrentOrigin.x " , mCurrentOrigin.x + "");
return true;
}
//快速滑动
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if ((mCurrentFlingDirection == Direction.LEFT && !mHorizontalFlingEnabled) ||
(mCurrentFlingDirection == RIGHT && !mHorizontalFlingEnabled)) {
return true;
}
mCurrentFlingDirection = mCurrentScrollDirection;
mScroller.forceFinished(true);
switch (mCurrentFlingDirection) {
case LEFT:
case RIGHT:
mScroller.fling((int) mCurrentOrigin.x, (int) mCurrentOrigin.y, (int) (velocityX * mXScrollingSpeed), 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
break;
case VERTICAL:
break;
}
ViewCompat.postInvalidateOnAnimation(CommentBar.this);
Log.e(" mCurrentOrigin.x2 " , mCurrentOrigin.x + "");
return true;
}
//单击事件
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return super.onSingleTapConfirmed(e);
}
//长按
@Override
public void onLongPress(MotionEvent e) {
super.onLongPress(e);
}
};
主要是处理快速滑动和滑动这两种手势,在快速移动的时候用到了OverScroer对象,这里的使用方法,在这里就不介绍了,这边主要是实现了他的一个回调方法,在回调方法处理相应的事件
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.isFinished()) {//当前滚动是否结束
if (mCurrentFlingDirection != Direction.NONE) {
// goToNearestBar();
}
} else {
if (mCurrentFlingDirection != Direction.NONE && forceFinishScroll()) { //惯性滑动时保证最左边条目展示正确
// goToNearestBar();
} else if (mScroller.computeScrollOffset()) {//滑动是否结束 记录最新的滑动的点 惯性滑动处理
mCurrentOrigin.y = mScroller.getCurrY();
mCurrentOrigin.x = mScroller.getCurrX();
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
最后就是view首次的加载动画,这里是创建差值器,不断的去改变x轴的偏移量
mValueAnimator = ValueAnimator.ofFloat(0,(getWidth() + xInterVal - (horText.length * bottomHeight + (horText.length ) * vertialInterval + startChart)),0);
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
{
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator)
{
Log.e(" valueAnimator ","" + valueAnimator.getAnimatedValue());
mCurrentOrigin.x = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
mValueAnimator.setDuration(2500);
mValueAnimator.start();
到这里所有效果就分析完毕,完整代码如下,大家结合之后就更加清晰易懂了
package com.socks.scrollerdemo;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.os.Build;
import android.support.v4.view.GestureDetectorCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutLinearInInterpolator;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.OverScroller;
import java.util.ArrayList;
import java.util.List;
import static com.socks.scrollerdemo.CommentBar.Direction.RIGHT;
/**
* Created by ${charles} on 2017/10/18.
*
* @desc ${TODO}
*/
public class CommentBar extends View
{
private float xInterVal = 100f;
private GestureDetectorCompat mGestureDetector;
private int mHeight;
private int mWidth;
private float textStart;
private int paddingTop = 20;
private float startChart = 60f; //柱子开始的横坐标
private float bottomHeight = 100f;//底部横坐标高度
private float vertialInterval;//柱子之间的间隔
private float barWidth = 100f;
private float yellowBarWidth = 60;
private float grayBarWidth = 20;
private float barInterval = 10;
private Paint mLinePaint;
private Paint mVertileTextpaint;
private Paint mChartPaint;
private Paint mGrayPaint;
private Rect mBound;
private Context mContext;
private String[] vertileText = {"0","2","4","6","8","10"};
private String[] horText = {"外观","油耗","空间","舒适度","动力","操控","故障率","内饰","性价比","隔音率"};
private boolean mHorizontalFlingEnabled = true;
private OverScroller mScroller;
//滑动速度
private float mXScrollingSpeed = 1f;
private int mMinimumFlingVelocity = 0;
private int mScrollDuration = 250;
public enum Direction {
NONE, LEFT, RIGHT, VERTICAL
}
//正常滑动方向
private Direction mCurrentScrollDirection = Direction.NONE;
//快速滑动方向
private Direction mCurrentFlingDirection = Direction.NONE;
private PointF mCurrentOrigin = new PointF(0f, 0f);
private ValueAnimator mValueAnimator;
private final GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {
//手指按下
@Override
public boolean onDown(MotionEvent e) {
// goToNearestBar();
return true;
}
//有效的滑动
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
switch (mCurrentScrollDirection) {
case NONE:
// 只允许在一个方向上滑动
if (Math.abs(distanceX) > Math.abs(distanceY)) {
if (distanceX > 0) {
mCurrentScrollDirection = Direction.LEFT;
} else {
mCurrentScrollDirection = RIGHT;
}
} else {
mCurrentScrollDirection = Direction.VERTICAL;
}
break;
case LEFT:
// Change direction if there was enough change.
if (Math.abs(distanceX) > Math.abs(distanceY) && (distanceX < 0)) {
mCurrentScrollDirection = RIGHT;
}
break;
case RIGHT:
// Change direction if there was enough change.
if (Math.abs(distanceX) > Math.abs(distanceY) && (distanceX > 0)) {
mCurrentScrollDirection = Direction.LEFT;
}
break;
}
// 重新计算滑动后的起点
switch (mCurrentScrollDirection) {
case LEFT:
case RIGHT:
mCurrentOrigin.x -= distanceX * mXScrollingSpeed;
ViewCompat.postInvalidateOnAnimation(CommentBar.this);
break;
}
Log.e(" mCurrentOrigin.x " , mCurrentOrigin.x + "");
return true;
}
//快速滑动
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if ((mCurrentFlingDirection == Direction.LEFT && !mHorizontalFlingEnabled) ||
(mCurrentFlingDirection == RIGHT && !mHorizontalFlingEnabled)) {
return true;
}
mCurrentFlingDirection = mCurrentScrollDirection;
mScroller.forceFinished(true);
switch (mCurrentFlingDirection) {
case LEFT:
case RIGHT:
mScroller.fling((int) mCurrentOrigin.x, (int) mCurrentOrigin.y, (int) (velocityX * mXScrollingSpeed), 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
break;
case VERTICAL:
break;
}
ViewCompat.postInvalidateOnAnimation(CommentBar.this);
Log.e(" mCurrentOrigin.x2 " , mCurrentOrigin.x + "");
return true;
}
//单击事件
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return super.onSingleTapConfirmed(e);
}
//长按
@Override
public void onLongPress(MotionEvent e) {
super.onLongPress(e);
}
};
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.isFinished()) {//当前滚动是否结束
if (mCurrentFlingDirection != Direction.NONE) {
// goToNearestBar();
}
} else {
if (mCurrentFlingDirection != Direction.NONE && forceFinishScroll()) { //惯性滑动时保证最左边条目展示正确
// goToNearestBar();
} else if (mScroller.computeScrollOffset()) {//滑动是否结束 记录最新的滑动的点 惯性滑动处理
mCurrentOrigin.y = mScroller.getCurrY();
mCurrentOrigin.x = mScroller.getCurrX();
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
/**
* Check if scrolling should be stopped.
*
* @return true if scrolling should be stopped before reaching the end of animation.
*/
private boolean forceFinishScroll() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
return mScroller.getCurrVelocity() <= mMinimumFlingVelocity;
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event)
{
//将view的OnTouchEvent事件交给手势监听器处理
boolean val = mGestureDetector.onTouchEvent(event);
// 正常滑动结束后 处理最左边的条目
if (event.getAction() == MotionEvent.ACTION_UP && mCurrentFlingDirection == Direction.NONE) {
if (mCurrentScrollDirection == RIGHT || mCurrentScrollDirection == Direction.LEFT) {
// goToNearestBar();
}
mCurrentScrollDirection = Direction.NONE;
}
return val;
}
public CommentBar(Context context)
{
super(context);
}
public CommentBar(Context context, AttributeSet attrs)
{
super(context, attrs);
mContext = context;
//初始化手势
initData();
mGestureDetector = new GestureDetectorCompat(context, mGestureListener);
// 解决长按屏幕后无法拖动的现象 但是 长按 用不了
mGestureDetector.setIsLongpressEnabled(false);
mScroller = new OverScroller(mContext, new FastOutLinearInInterpolator() );
textStart = startChart + xInterVal + (barWidth / 2f);
mBound = new Rect();
mLinePaint = new Paint();
mLinePaint.setColor(Color.RED);
mLinePaint.setAntiAlias(true);
mLinePaint.setStyle(Paint.Style.FILL);
mLinePaint.setStrokeWidth(3);
mVertileTextpaint = new Paint();
mVertileTextpaint.setAntiAlias(true);
mVertileTextpaint.setColor(Color.RED);
mVertileTextpaint.setTextSize(36);
mChartPaint = new Paint();
mChartPaint.setAntiAlias(true);
mChartPaint.setColor(Color.YELLOW);
mChartPaint.setStyle(Paint.Style.FILL);
mLinePaint.setStrokeWidth(3);
mGrayPaint = new Paint();
mGrayPaint.setAntiAlias(true);
mGrayPaint.setColor(Color.GRAY);
mGrayPaint.setTextSize(36);
mValueAnimator = ValueAnimator.ofFloat(0,(getWidth() + xInterVal - (horText.length * bottomHeight + (horText.length ) * vertialInterval + startChart)),0);
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
{
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator)
{
Log.e(" valueAnimator ","" + valueAnimator.getAnimatedValue());
mCurrentOrigin.x = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
mValueAnimator.setDuration(2500);
mValueAnimator.start();
mValueAnimator.addListener(new Animator.AnimatorListener()
{
@Override
public void onAnimationStart(Animator animator)
{
}
@Override
public void onAnimationEnd(Animator animator)
{
}
@Override
public void onAnimationCancel(Animator animator)
{
}
@Override
public void onAnimationRepeat(Animator animator)
{
}
});
}
public CommentBar(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//宽度的模式
int mWidthModle = MeasureSpec.getMode(widthMeasureSpec);
//宽度大小
int mWidthSize = MeasureSpec.getSize(widthMeasureSpec);
int mHeightModle = MeasureSpec.getMode(heightMeasureSpec);
int mHeightSize = MeasureSpec.getSize(heightMeasureSpec);
//如果明确大小,直接设置大小
if (mWidthModle == MeasureSpec.EXACTLY) {
mWidth = mWidthSize;
} else {
//计算宽度,可以根据实际情况进行计算
mWidth = (getPaddingLeft() + getPaddingRight());
//如果为AT_MOST, 不允许超过默认宽度的大小
if (mWidthModle == MeasureSpec.AT_MOST) {
mWidth = Math.min(mWidth, mWidthSize);
}
}
if (mHeightModle == MeasureSpec.EXACTLY) {
mHeight = mHeightSize;
} else {
mHeight = (getPaddingTop() + getPaddingBottom());
if (mHeightModle == MeasureSpec.AT_MOST) {
mHeight = Math.min(mHeight, mHeightSize);
}
}
//设置测量完成的宽高
setMeasuredDimension(mWidth, mHeight);
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
//控制图表滑动左右边界
if (mCurrentOrigin.x < getWidth() + xInterVal - (horText.length * bottomHeight + (horText.length ) * vertialInterval + startChart) )
mCurrentOrigin.x = getWidth() + xInterVal - (horText.length * bottomHeight + (horText.length ) * vertialInterval + startChart) ;
Log.e(" ---- ",(horText.length * bottomHeight + (horText.length - 1) * vertialInterval + startChart) + " getWidth() " + getWidth());
if (mCurrentOrigin.x > 0)
mCurrentOrigin.x = 0;
float textHeight = mHeight + paddingTop - bottomHeight;//横坐标高度
vertialInterval = (textHeight - 200f) / 5;
canvas.drawLine(startChart,0,startChart,textHeight,mLinePaint);
canvas.drawLine(startChart,textHeight,mWidth,textHeight,mLinePaint);
for (int i=0; i < vertileText.length; i++){
float y = (vertialInterval*i);
mVertileTextpaint.getTextBounds(vertileText[i],0,vertileText[i].length(),mBound);
canvas.drawText(vertileText[i],0,textHeight - y + mBound.height() / 2,mVertileTextpaint);
if (i > 0)
canvas.drawLine(startChart,textHeight - y,startChart + 10,textHeight - y,mVertileTextpaint);
}
canvas.clipRect(startChart + 10, 0, mWidth, getHeight(), Region.Op.REPLACE);
float chartTempStart = startChart + xInterVal;
float maxY = textHeight - 200f;
for (int i=0; i < horText.length; i++){
float height = 200f + (10f - barDatas.get(i).getNum()) / 10f * maxY;
String num = String.valueOf(barDatas.get(i).getNum());
mVertileTextpaint.getTextBounds(num,0,num.length(),mBound);
canvas.drawText(num,chartTempStart + mCurrentOrigin.x,height - mBound.height(),mVertileTextpaint);
RectF rectF = new RectF();
rectF.left = chartTempStart + mCurrentOrigin.x;
rectF.top = height;
rectF.right = chartTempStart + yellowBarWidth + mCurrentOrigin.x;
rectF.bottom = textHeight - 3;
canvas.drawRect(rectF,mChartPaint);
chartTempStart += ( barWidth + startChart);
}
float grayBarStart = startChart + xInterVal ;
for (int i=0; i < horText.length; i++){
float height = 200f + (10f - avgDatas.get(i).getNum()) / 10f * maxY;
RectF rectF = new RectF();
rectF.left = grayBarStart + (yellowBarWidth + barInterval) + mCurrentOrigin.x;
rectF.top = height;
rectF.right = grayBarStart + (yellowBarWidth + barInterval) + grayBarWidth + mCurrentOrigin.x;
rectF.bottom = textHeight - 3;
canvas.drawRect(rectF,mGrayPaint);
grayBarStart += (barWidth + startChart );
}
float textTempStart = textStart;
for (int i=0; i < horText.length; i++){
mVertileTextpaint.getTextBounds(horText[i],0,horText[i].length(),mBound);
canvas.drawText(horText[i],textTempStart - mBound.width()/2 + mCurrentOrigin.x,mHeight - bottomHeight + mBound.height() + 38f,mVertileTextpaint);
textTempStart += (bottomHeight + startChart);
}
}
/**private void goToNearestBar() {
//让最左边的条目 显示出来
double leftBar = mCurrentOrigin.x / (bottomHeight + startChart);
if (mCurrentFlingDirection != Direction.NONE) {
// 跳到最近一个bar
leftBar = Math.round(leftBar);
} else if (mCurrentScrollDirection == Direction.LEFT) {
// 跳到上一个bar
leftBar = Math.floor(leftBar);
} else if (mCurrentScrollDirection == RIGHT) {
// 跳到下一个bar
leftBar = Math.ceil(leftBar);
} else {
// 跳到最近一个bar
leftBar = Math.round(leftBar);
}
int nearestOrigin = (int) (mCurrentOrigin.x - leftBar * (bottomHeight + startChart));
if (nearestOrigin != 0) {
// 停止当前动画
mScroller.forceFinished(true);
//开始滚动
mScroller.startScroll((int) mCurrentOrigin.x, (int) mCurrentOrigin.y, -nearestOrigin, 0, (int) (Math.abs(nearestOrigin) / (bottomHeight + startChart) * mScrollDuration));
ViewCompat.postInvalidateOnAnimation(CommentBar.this);
}
//重新设置滚动方向.
mCurrentScrollDirection = mCurrentFlingDirection = Direction.NONE;
}*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mWidth = getWidth();
mHeight = getHeight() - paddingTop;
}
private List<BarData> barDatas = new ArrayList<>();
private List<BarData> avgDatas = new ArrayList<>();
private void initData(){
barDatas.add(new BarData("外观",9.3f));
barDatas.add(new BarData("油耗",8.1f));
barDatas.add(new BarData("空间",9.5f));
barDatas.add(new BarData("舒适度",8.8f));
barDatas.add(new BarData("动力",8.5f));
barDatas.add(new BarData("操控",8.6f));
barDatas.add(new BarData("故障率",0.7f));
barDatas.add(new BarData("内饰",8.6f));
barDatas.add(new BarData("性价比",8.3f));
barDatas.add(new BarData("隔音率",0.7f));
avgDatas.add(new BarData("外观",9.1f));
avgDatas.add(new BarData("油耗",7.7f));
avgDatas.add(new BarData("空间",8.8f));
avgDatas.add(new BarData("舒适度",8.6f));
avgDatas.add(new BarData("动力",8.6f));
avgDatas.add(new BarData("操控",8.6f));
avgDatas.add(new BarData("故障率",2.6f));
avgDatas.add(new BarData("内饰",8.5f));
avgDatas.add(new BarData("性价比",8.2f));
avgDatas.add(new BarData("隔音率",2.5f));
}
}