項目源碼
https://github.com/dogmeng/littleyunmusic
第二部分 自定義控件的實現(xiàn)
主要有主頁滑動條MoveLine,播放頁面PlayRoundView,歌詞頁面LrcView,及一些簡單的自定義輸入框LoginEditText和自定義圓形或圓角矩形CircleImageView的實現(xiàn).
MoveLine和PlayRoundView的實現(xiàn)過程都用到了貝塞爾曲線(二階和三階),相關(guān)文章也很多,這里不再一一說明.重點介紹控件的實現(xiàn)思想.
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);
}
//由外界傳入,開始位置,結(jié)束位置,和y軸的縮放比例
public void setPosition(float startX,float endX,float speed){
this.startX = startX;
this.endX = endX;
this.controlY = height*speed;
invalidate();
}
因為本項目中的滑動是有頁面中的viewpager控制的,所以就在viewpager的滑動監(jiān)聽中,來設(shè)置這幾個參數(shù)
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:當(dāng)前頁面 arg1:當(dāng)前頁面偏移百分比 arg2:當(dāng)前頁面偏移的像素位置
//右滑從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 突變?yōu)?,position為小的,突變?yōu)榇蟮? 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:整體是三個圓形的疊加,圓形的一半沿著隨機的方向向外擴展,到一定距離后,出現(xiàn)不規(guī)則分布的小點點.在這里,圓形外擴的參數(shù)由屬性動畫來控制,然后對canvas進行隨機旋轉(zhuǎn),這樣坐標(biāo)參數(shù)就比較簡單,容易控制.
首先初始化畫筆和圓形坐標(biāo)
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;
//初始化圓形坐標(biāo)
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);
//設(shè)置小點點出現(xiàn)的區(qū)域
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);
//移動坐標(biāo)中心到圓心
canvas.translate(defaultwidth/2, width/2+defaultwidth/8);
canvas.save();
//旋轉(zhuǎn)坐標(biāo)軸
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) {
//根據(jù)屬性動畫提供的move值,進行動態(tài)繪制
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:歌詞滾動控件,因目前未實現(xiàn)網(wǎng)路下載功能,所以歌詞暫且用測試數(shù)據(jù)代替,來演示效果.
歌詞的滾動來自兩個方面的控制,一是播放時,自動滾動到當(dāng)前播放行,這個可以用一個handler每隔1秒發(fā)送message來檢測當(dāng)前播放時間和歌詞所在行時間,如果匹配,就設(shè)置當(dāng)前行為中心行.另一個是用手指滑動來設(shè)置當(dāng)前行.onDraw的重點在于從中心行開始繪制,然后畫上半部分和下半部分,這樣可以避免繪制無用的頁面外的行.繪制過程中根據(jù)滑動距離,不斷移動橫坐標(biāo)的位置.當(dāng)移動到下一行后,重置橫坐標(biāo)位置,然后繼續(xù)繪制.整個過程就是這樣反復(fù)進行.具體如下:
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//重置數(shù)據(jù)
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);
//當(dāng)移動距離大于兩行之間的高度時,重新設(shè)置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為負(fù),下移
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);
}
}