3D版翻页公告效果

code小生,一个专注于 Android 领域的技术分享平台

作者:的一幕
地址:https://www.jianshu.com/p/caa5f38d393a
声明:本文是 的一幕 原创,转发等请联系原作者授权。

前言:

在逛小程序蘑菇街的时候,看到一个2D版滚动的翻页公告效果。其实看到这个效果的时候,一点都不觉得稀奇,因为之前也见过类似的。效果如下:

蘑菇街效果.gif

这里因为学习了3D平面的旋转,因此我自己也撸出了一个3D版的翻页公告效果:

simple.gif

是不是一下子觉得有趣多了呢,那就赶紧和我去看下如何做出这种效果吧 。

使用:

布局:

<!--指定从下到上翻滚-->
<com.xiangcheng.marquee3dview.Marquee3DView
   android:id="@+id/marquee3DView"
   android:layout_width="wrap_content"
   android:layout_height="25dp"
   android:layout_marginBottom="8dp"
   android:layout_marginEnd="8dp"
   android:layout_marginStart="8dp"
   android:layout_marginTop="8dp"
   app:direction="D2U"
   app:layout_constraintBottom_toBottomOf="parent"
   app:layout_constraintEnd_toEndOf="parent"
   app:layout_constraintStart_toStartOf="parent"
   app:layout_constraintTop_toTopOf="parent" />


<LinearLayout
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:layout_marginBottom="8dp"
   android:layout_marginTop="8dp"
   android:background="#FFC0CB"
   android:gravity="center"
   app:layout_constraintBottom_toBottomOf="parent"
   app:layout_constraintEnd_toEndOf="@+id/marquee3DView"
   app:layout_constraintStart_toStartOf="@+id/marquee3DView"
   app:layout_constraintTop_toBottomOf="@+id/marquee3DView">

   <!--从上到下翻滚-->
   <com.xiangcheng.marquee3dview.Marquee3DView
       android:id="@+id/marquee3DView2"
       android:layout_width="wrap_content"
       android:layout_height="40dp"
       android:layout_marginStart="10dp"
       app:back_color="#00ffffff"
       app:direction="U2D"
       app:highlight_color="#FF6347"
       app:highlight_position="3"
       app:rotate_duration="1500"
       app:show_duration="1500" />

</LinearLayout>

属性:

<declare-styleable name="Marquee3DView">
   <!--指定旋转的方向-->
   <attr name="direction" format="enum">
       <!--从上到下-->
       <enum name="U2D" value="2" />
       <!--从下到上-->
       <enum name="D2U" value="1" />
   </attr>
   <!--高亮的item位置-->
   <attr name="highlight_position" format="integer" />
   <!--item的颜色-->
   <attr name="back_color" format="color" />
   <!--高亮的文字、下划线颜色-->
   <attr name="highlight_color" format="color" />
   <!--3D旋转的时间-->
   <attr name="rotate_duration" format="integer" />
   <!--停留显示的时间-->
   <attr name="show_duration" format="integer" />
   <!--右边文字的颜色-->
   <attr name="label_text_color" format="color" />
   <!--右边文字的大小-->
   <attr name="label_text_size" format="dimension" />
   <!--指定左边图片的半径-->
   <attr name="label_bitmap_radius" format="dimension" />
   <!--bitmap和text之间的间距-->
   <attr name="label_bitmap_text_offset" format="dimension" />
</declare-styleable>

代码:

/**
* 设置显示的label
* @param marqueeLabels
*/

public void setMarqueeLabels(List<String> marqueeLabels)

/**
* 设置显示的bitmap
* @param labelBitmap
*/

public void setLabelBitmap(List<Bitmap> labelBitmap)

/**
* 点击监听
*
*/

setOnWhereItemClick(new Marquee3DView.OnWhereItemClick()
{
   @Override
   public void onItemClick(int position) {
       //TODO
   }
});

gradle:

compile 'com.xiangcheng:marquee3dlibs:1.0.1'

maven:

<dependency>
 <groupId>com.xiangcheng</groupId>
 <artifactId>marquee3dlibs</artifactId>
 <version>1.0.1</version>
 <type>pom</type>
</dependency>

讲解:

初始化属性

private void initArgus(Context context, AttributeSet attrs) {
   TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Marquee3DView);
   backColor = typedArray.getColor(R.styleable.Marquee3DView_back_color, Color.parseColor("#cccccc"));
   direction = typedArray.getInt(R.styleable.Marquee3DView_direction, D2U);
   highLightPosition = typedArray.getInt(R.styleable.Marquee3DView_highlight_position, highLightPosition);
   highLightColor = typedArray.getColor(R.styleable.Marquee3DView_highlight_color, Color.parseColor("#FF1493"));
   rotateDuration = typedArray.getInt(R.styleable.Marquee3DView_rotate_duration, rotateDuration);
   showDuration = typedArray.getInt(R.styleable.Marquee3DView_show_duration, showDuration);
   labelColor = typedArray.getColor(R.styleable.Marquee3DView_label_text_color, Color.parseColor("#778899"));
   labelTextSize = (int) typedArray.getDimension(R.styleable.Marquee3DView_label_text_size, sp2px(15));
   labelBitmapRadius = (int) typedArray.getDimension(R.styleable.Marquee3DView_label_bitmap_radius, dp2px(10));
   labelBitmapTextOffset = (int) typedArray.getDimension(R.styleable.Marquee3DView_label_bitmap_text_offset, dp2px(10));
}

初始化变量

private void initialize() {
   camera = new Camera();
   matrix = new Matrix();

   textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   textPaint.setTextSize(labelTextSize);
   textPaint.setColor(labelColor);

   currentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   currentPaint.setTextSize(labelTextSize);
   currentPaint.setColor(labelColor);

   nextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   nextPaint.setTextSize(labelTextSize);
   nextPaint.setColor(labelColor);

   linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   linePaint.setColor(highLightColor);
   linePaint.setStrokeWidth(dp2px(1));
   linePaint.setStyle(Paint.Style.FILL);

   highLightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   highLightPaint.setTextSize(sp2px(15));
   highLightPaint.setColor(highLightColor);

   textRegion = new Region();

   mBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   mBitmapPaint.setColor(Color.WHITE);
   mBitmapPaint.setStrokeWidth(0);
}

初始化动画

private void initAnimation() {
   showItemRunable = new ShowItemRunable();
   //角度变化是0到90度的区间
   rotateAnimator = ValueAnimator.ofFloat(0, 90);
   rotateAnimator.setDuration(rotateDuration);
   rotateAnimator.setInterpolator(new LinearInterpolator());
   rotateAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
       @Override
       public void onAnimationUpdate(ValueAnimator animation) {
           isRunning = true;
           //当前变化的角度变量,在绘制的时候使用
           changeRotate = (float) animation.getAnimatedValue();
           //计算当前的画笔的透明度(从255到0的过程)
           caculateCurrentPaint(changeRotate);
           //计算下一个item的画笔透明度(从0到255的过程)
           caculateNextPaint(changeRotate);
           //从0到height的一个过程,这里因为旋转的时候,同时还要进行平移
           translateY = height * animation.getAnimatedFraction();
           invalidate();
       }
   });
   //处理旋转结束后,停留一会显示
   rotateAnimator.addListener(new AnimatorListenerAdapter() {
       @Override
       public void onAnimationEnd(Animator animation) {
           super.onAnimationEnd(animation);
           isRunning = false;
           postDelayed(showItemRunable, showDuration);
       }
   });
   //刚进来的时候,在第一个item上进行停留
   startRunable = new StartRunable();
   postDelayed(startRunable, showDuration);
}

//停留显示完的操作
private class ShowItemRunable implements Runnable {
   @Override
   public void run() {
       currentItem++;
       if (currentItem >= marqueeLabels.size()) {
           currentItem = 0;
       }
       rotateAnimator.start();
   }
}

//刚进来时第一个item显示完后的操作
private class StartRunable implements Runnable {
   @Override
   public void run() {
       hasStart = true;
       rotateAnimator.start();
   }
}

//当前画笔透明度的改变(255——>0)
private void caculateCurrentPaint(float rotateAngle) {
   float percent = rotateAngle / 90;
   int alpha = (int) (255 - percent * 255);
   currentPaint.setAlpha(alpha);
}

//下一个item的画笔透明度的改变(0——>255)
private void caculateNextPaint(float rotateAngle) {
   float percent = rotateAngle / 90;
   int alpha = (int) (percent * 255);
   nextPaint.setAlpha(alpha);
}

上面动画部分,其实你要关心的就是两个变量:changeRotate(0——>90度变化)、translateY(0——>height变化)

绘制

@Override
protected void onDraw(Canvas canvas) {
   super.onDraw(canvas);
   if (marqueeLabels == null || marqueeLabels.size() <= 0) {
       return;
   }
   drawCurrentItem(canvas);
   drawNextItem(canvas);
}
private void drawCurrentItem(Canvas canvas) {
   canvas.save();
   camera.save();
   if (direction == D2U) {
       //当前的item从下到上转动,逆时针旋转,角度是增大的过程
       camera.rotateX(changeRotate);
   } else {
       //从上到下旋转,顺时针旋转,角度是负角
       camera.rotateX(-changeRotate);
   }
   camera.getMatrix(matrix);
   camera.restore();
   if (direction == D2U) {
       将旋转中心至为下面一条边的中点上
       matrix.preTranslate(-width / 2, -height);
       //这里由于当前的item是往上转动的,下面的一条边最后是在0的位置了
       matrix.postTranslate(width / 2, height - translateY);
   } else {
       //这里如果是往下转动时,旋转中心就是上面一条边的中点了
       matrix.preTranslate(-width / 2, 0);
       //往下转动时,上面的边是不断地往下移动的,因此y轴是增大的
       matrix.postTranslate(width / 2, translateY);
   }
   //创建绘制的内容
   textBitmap = createChild(currentItem, false);
   canvas.drawBitmap(textBitmap, matrix, null);
   canvas.restore();
}

//这里用到了隔离绘制,将最后要画的东西都放到了bitmap上
private Bitmap createChild(int position, boolean isNext) {
   Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
   Canvas canvas = new Canvas(bitmap);
   canvas.drawColor(backColor);
   if (labelBitmap != null && labelBitmap.size() > 0) {
       //绘制bitmap
       drawLabelBitmap(canvas, position);
   }
   Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
   float allHeight = fontMetrics.descent - fontMetrics.ascent;
   float textWidth = textPaint.measureText(marqueeLabels.get(position));
   Rect rect = new Rect();
   rect.left = (int) labelTextStart;
   rect.right = (int) (labelTextStart + textWidth);
   rect.top = (int) (height / 2 - allHeight / 2);
   rect.bottom = (int) (height / 2 + allHeight / 2);
   textRegion.set(rect);
   //这里分是不是绘制下一个item
   if (isNext) {
       //如果是高亮的item,需要绘制下划线,以及改为高亮画笔
       if (highLightPosition == position) {
           caculateHighLightPaint(changeRotate, true);
           canvas.drawText(marqueeLabels.get(position), labelTextStart, height / 2 - allHeight / 2 - fontMetrics.ascent, highLightPaint);
           canvas.drawLine(labelTextStart, (float) (height * 1.0 / 2 + allHeight / 2),
                   labelTextStart + textWidth, (float) (height * 1.0 / 2 + allHeight / 2), linePaint);
       } else {
           canvas.drawText(marqueeLabels.get(position), labelTextStart, height / 2 - allHeight / 2 - fontMetrics.ascent, nextPaint);
       }
   } else {
       if (highLightPosition == position) {
           caculateHighLightPaint(changeRotate, false);
           canvas.drawText(marqueeLabels.get(position), labelTextStart, height / 2 - allHeight / 2 - fontMetrics.ascent, highLightPaint);
           canvas.drawLine(labelTextStart, (float) (height * 1.0 / 2 + allHeight / 2),
                   labelTextStart + textWidth, (float) (height * 1.0 / 2 + allHeight / 2), linePaint);
       } else {
           canvas.drawText(marqueeLabels.get(position), labelTextStart, height / 2 - allHeight / 2 - fontMetrics.ascent, currentPaint);
       }
   }
   return bitmap;
}

//绘制左边的bitmap
private void drawLabelBitmap(Canvas canvas, int position) {
   int layer = canvas.saveLayer(0, 0, width, height, null, Canvas.ALL_SAVE_FLAG);
   //先画圆,dst层
   canvas.drawCircle(labelBitmapRadius, height / 2, labelBitmapRadius, mBitmapPaint);
   //该mode下取两部分的交集部分
   mBitmapPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
   //src层
   canvas.drawBitmap(labelBitmap.get(position), 0, height / 2 - labelBitmapRadius, mBitmapPaint);
   mBitmapPaint.setXfermode(null);
   canvas.restoreToCount(layer);
   labelTextStart = labelBitmapRadius * 2 + labelBitmapTextOffset;
}

//计算高亮的画笔的透明度,跟普通的画笔一样的算法
private void caculateHighLightPaint(float rotate, boolean isNext) {
   if (isNext) {
       float percent = rotate / 90;
       int alpha = (int) (percent * 255);
       highLightPaint.setAlpha(alpha);
       linePaint.setAlpha(alpha);
   } else {
       float percent = rotate / 90;
       int alpha = (int) (255 - percent * 255);
       highLightPaint.setAlpha(alpha);
       linePaint.setAlpha(alpha);
   }
}

private void drawNextItem(Canvas canvas) {
   caculateNextItem();
   canvas.save();
   camera.save();
   if (direction == D2U) {
       //从下到上时,另外一个面初始位置是-90度,最后趋于0度位置
       camera.rotateX(-90 + changeRotate);
   } else {
       //从上到下是90度到0度的过程
       camera.rotateX(90 - changeRotate);
   }
   camera.getMatrix(matrix);
   camera.restore();
   if (direction == D2U) {
       //从下到上,旋转点是上面一条边的中点
       matrix.preTranslate(-width / 2, 0);
       //初始位置是height,最后到了0的位置
       matrix.postTranslate(width / 2, height + (-translateY));
   } else {
       //从上到下,旋转点是下面一条边的中点
       matrix.preTranslate(-width / 2, -height);
       //初始位置是0,最后到了height位置
       matrix.postTranslate(width / 2, translateY);
   }
   textBitmap = createChild(nextItem, true);
   canvas.drawBitmap(textBitmap, matrix, null);
   canvas.restore();
}
从上到下旋转示意图.png从下到上旋转示意图.png

这里给出了两种情况旋转前、旋转后的示意图,上面的平行四边形都是一个平面,可以想象下。

其实讲解到这就基本没什么了,再就是一些细节性的代码了。如果有什么不明白的地方,可以互相交流。

总结:

(一):初始化一些需要的变量
(二):初始化动画变量
(三):绘制两个翻转的平面

thanks:
https://github.com/zhangyuChen1991/Roll3DImageView

代码传送门
https://github.com/xiangcman/Marquee3DView-master


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值