项目源码
https://github.com/dogmeng/littleyunmusic
第二部分 自定义控件的实现
主要有主页滑动条MoveLine,播放页面PlayRoundView,歌词页面LrcView,及一些简单的自定义输入框LoginEditText和自定义圆形或圆角矩形CircleImageView的实现.
MoveLine和PlayRoundView的实现过程都用到了贝塞尔曲线(二阶和三阶),相关文章也很多,这里不再一一说明.重点介绍控件的实现思想.
MoveLine:随手指滑动的距离,由弧形变直线再变弧形.也就是说,滑动距离影响控制弧形的三个点的位置.
如图,弧形部分即为moveline
在MoveLine的onDraw方法中:
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
mPath.reset();
//渐变
mShader = new LinearGradient(startX, 0, startX, controlY/2, tabColor, themeColor, Shader.TileMode.CLAMP);
mPaint.setShader(mShader);
//绘制弧形
mPath.moveTo(startX, 0);
mPath.quadTo((endX-startX)/2+startX, controlY, endX, 0);
canvas.drawPath(mPath, mPaint);
}
//由外界传入,开始位置,结束位置,和y轴的缩放比例
public void setPosition(float startX,float endX,float speed){
this.startX = startX;
this.endX = endX;
this.controlY = height*speed;
invalidate();
}
因为本项目中的滑动是有页面中的viewpager控制的,所以就在viewpager的滑动监听中,来设置这几个参数
class MainViewPagerListener implements ViewPager.OnPageChangeListener{
private int lastPosition = -1;
@Override
public void onPageScrollStateChanged(int arg0) {
// TODO Auto-generated method stub
//0:挂起 1:正在滑动 2:滑动完毕
}
@Override
public void onPageScrolled(int arg0, final float arg1, int arg2) {
// TODO Auto-generated method stub
//agr0:当前页面 arg1:当前页面偏移百分比 arg2:当前页面偏移的像素位置
//右滑从1到0,position为小的
if(arg2!=0&&arg2 < lastPosition){
if(arg1>0.5f){
startX = (int) (sideWidth+ tabWidth*(arg0+1)-(tabWidth*(1-arg1)*2));
endX = (int) (sideWidth+ tabWidth*(arg0+2));
moveLine.setPosition(startX,endX, (2*arg1-1));
}else if(arg1<=0.5f){
startX = (int) (sideWidth+ tabWidth*arg0);
endX = (int) (sideWidth+ tabWidth*(arg0+2)-(tabWidth*(1-2*arg1)));
moveLine.setPosition(startX,endX, (1-2*arg1));
}
}
//左滑从0到1 突变为0,position为小的,突变为大的
if(arg2!=0&&arg2 > lastPosition){
if(arg1<0.5f){
startX =(int) (sideWidth+ tabWidth*arg0);
endX =(int) (sideWidth+tabWidth*(arg0+1)+(tabWidth*arg1*2));
moveLine.setPosition(startX,endX, (1-2*arg1));
}else if(arg1>=0.5f){
endX = (int)(sideWidth+tabWidth*(arg0+2));
startX =(int) (sideWidth+ tabWidth*arg0+(tabWidth*(2*arg1-1)));
moveLine.setPosition(startX,endX, (2*arg1-1));
}
}
lastPosition = arg2;
}
PlayRoundView:整体是三个圆形的叠加,圆形的一半沿着随机的方向向外扩展,到一定距离后,出现不规则分布的小点点.在这里,圆形外扩的参数由属性动画来控制,然后对canvas进行随机旋转,这样坐标参数就比较简单,容易控制.
首先初始化画笔和圆形坐标
private void init(){
themeColor = ThemeManager.getCurrentColor(context);
//初始化圆形画笔
mPaint = new Paint();
//初始化小点点画笔
starPaint = new Paint();
starPaint.setAntiAlias(true);
starPaint.setStrokeWidth(10);
starPaint.setStrokeCap(Cap.ROUND);
starPaint.setColor(themeColor);
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(themeColor);
mPath = new Path();
WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
defaultwidth = wm.getDefaultDisplay().getWidth();
defaultheight = wm.getDefaultDisplay().getHeight()*2/3;
mRadius = defaultwidth/6;
//初始化圆形坐标
p5 = new PointF(mRadius * bezFactor,-mRadius);
p6 = new PointF(0, -mRadius);
p7 = new PointF(-mRadius * bezFactor, -mRadius);
p0 = new PointF(0, mRadius);
p1 = new PointF(mRadius * bezFactor, mRadius);
p11 = new PointF(-mRadius * bezFactor, mRadius);
p2 = new PointF(mRadius, mRadius * bezFactor);
p3 = new PointF(mRadius, 0);
p4 = new PointF(mRadius, -mRadius * bezFactor);
p8 = new PointF(-mRadius, -mRadius * bezFactor);
p9 = new PointF(-mRadius, 0);
p10 = new PointF(-mRadius, mRadius * bezFactor);
//设置小点点出现的区域
starRect = new RectF(mRadius, -mRadius, defaultwidth/4, mRadius);
//初始化小点点的集合
for(int i = 0;i<16;i++){
starList.add(new PointF());
}
}
在onDraw()中进行绘制:
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
//移动坐标中心到圆心
canvas.translate(defaultwidth/2, width/2+defaultwidth/8);
canvas.save();
//旋转坐标轴
canvas.rotate(rotate);
//如果到达顶点则显示小点点
if(showStar){
for(int i = 0;i<starList.size()/2;i++){
starPaint.setAlpha((int) (255*move/drag));
canvas.drawPoint(starList.get(i++).x, starList.get(i++).y, starPaint);
starPaint.setAlpha((int) (150*move/drag));
canvas.drawPoint(starList.get(i).x, starList.get(i).y, starPaint);
}
}
//画圆形
bounce2RightRound(canvas);
canvas.restore();
}
private void bounce2RightRound(Canvas canvas) {
//根据属性动画提供的move值,进行动态绘制
for(int i = 0;i<3;i++){
mPaint.setAlpha(100+50*i);
mPath.reset();
mPath.moveTo(p0.x, p0.y-stroke*i);
mPath.cubicTo(p1.x-stroke*i, p1.y-stroke*i, p2.x-stroke*i+move, p2.y-stroke*i, p3.x -stroke*i+move, p3.y);
mPath.cubicTo(p4.x-stroke*i+move, p4.y+stroke*i, p5.x-stroke*i, p5.y +stroke*i, p6.x, p6.y +stroke*i);
mPath.cubicTo(p7.x+stroke*i, p7.y+stroke*i, p8.x+stroke*i, p8.y+stroke*i, p9.x+stroke*i, p9.y);
mPath.cubicTo(p10.x+stroke*i, p10.y-stroke*i, p11.x+stroke*i, p11.y-stroke*i, p0.x, p0.y-stroke*i);
mPath.close();
canvas.drawPath(mPath, mPaint);
}
}
属性动画中的move值
class DragAnimator extends BaseAnimator{
private float lastValue = 0;
private int count = 0;
public DragAnimator(View target, float startValue, float endValue,float thirdValue) {
super(target, startValue, endValue,thirdValue);
// TODO Auto-generated constructor stub
}
@Override
protected void doAnim(float animatedValue) {
// TODO Auto-generated method stub
move = animatedValue;
if(move-lastValue<0){
showStar = true;
}else{
showStar = false;
}
lastValue = move;
PlayRoundView.this.invalidate();
}
}
LrcView:歌词滚动控件,因目前未实现网路下载功能,所以歌词暂且用测试数据代替,来演示效果.
歌词的滚动来自两个方面的控制,一是播放时,自动滚动到当前播放行,这个可以用一个handler每隔1秒发送message来检测当前播放时间和歌词所在行时间,如果匹配,就设置当前行为中心行.另一个是用手指滑动来设置当前行.onDraw的重点在于从中心行开始绘制,然后画上半部分和下半部分,这样可以避免绘制无用的页面外的行.绘制过程中根据滑动距离,不断移动横坐标的位置.当移动到下一行后,重置横坐标位置,然后继续绘制.整个过程就是这样反复进行.具体如下:
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//重置数据
if(isFirst){
distanceY = 0;
isFirst = false;
}
if(isLrc()){
if(distanceY>0){
isTop = true;
if(centerLine == currentLrc.size()-1){
return false;
}
}else if(distanceY<0){
isTop = false;
if(centerLine == 0){
offset = 0;
m = 0;
return false;
}
}
//总移动距离
mOffset += distanceY;
offset = Math.abs(mOffset);
//中心行高度
int x = (int) staticLayouts.get(centerLine).getHeight();
y = Math.abs(offset-m);
//当移动距离大于两行之间的高度时,重新设置centerLine和y的值
if(y -(x+textSize)>=0){
if(isTop){
m = offset;
centerLine = centerLine+1 >= currentLrc.size()-1 ? currentLrc.size()-1:centerLine+1;
}else{
m = offset;
centerLine = centerLine-1 <= 0 ? 0:centerLine-1;
}
y = 0;
}else{
if(!isTop){
y=-y;
}
}
invalidate();
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
canvas.translate(getPaddingLeft(), height/2);
if(isLrc()){
//画中间的 y为正上移,y为负,下移
canvas.save();
canvas.translate(0, -y);
mContentPaint.setTextSize((float)(textSize*1.1));
mContentPaint.setColor(themeColor);
staticLayouts.get(centerLine).draw(canvas);
canvas.restore();
mContentPaint.setTextSize(textSize);
mContentPaint.setColor(getResources().getColor(R.color.toolbarTextColor));
//画上面的
if(centerLine>0){
int num = centerLine -1;
int top = 0;
while(top<height/2&&num>=0){
canvas.save();
top += 120+staticLayouts.get(num).getHeight();
canvas.translate(0, -top-y);
staticLayouts.get(num--).draw(canvas);
canvas.restore();
}
}
//画下面的
if(centerLine<currentLrc.size()-1){
Log.i("下面的centerLine", centerLine+"");
int num1 = centerLine;
int bottom = 0;
while(bottom<height/2&&num1<currentLrc.size()-1){
canvas.save();
bottom += 120+staticLayouts.get(num1).getHeight();
canvas.translate(0, bottom-y);
staticLayouts.get(++num1).draw(canvas);
canvas.restore();
}
}
}else{
textSize = 70;
mContentPaint.setTextSize(textSize);
mContentPaint.setTextAlign(Align.CENTER);
mContentPaint.setColor(themeColor);
canvas.drawText("暂无歌词", 0, 0, mContentPaint);
}
}