本文是学习记录文章
1、打造下拉放大效果
2、下拉放大部分,并且有下拉转圈圈的
3、卫星菜单
4、自定义带入场动画的弧形百分比进度条
5、制作雷达图
6、实现流动标签布局
7、声音录制
8、拖动控件到随意位置
9、模仿ViewPager
10、NestedScrollingParent2
第一章学习文章地址:【Android】打造下拉放大效果
俗话说学好数理化走遍天下都不怕,现在是学好自定义走遍android都不怕
原文作者在demo里面一共写了3个demo,我要自己一边学习作者的思想,一边自己跟着思想来敲代码。
第一个效果学习总结
知识摘要
(1)
xuexiFlexibleLayout flexibleLayout = (xuexiFlexibleLayout) findViewById(R.id.fv);
flexibleLayout.setReadyListener(new OnReadyPullListener() {
@Override
public boolean isReady() {
return manager.findFirstCompletelyVisibleItemPosition() == 0;
}
});
flexibleLayout.setHeader(headerImage);
findFirstCompletelyVisibleItemPosition,是获取recyclerview里面第一个参数完全可见,如果此时的位置是0,在onInterceptTouchEvent里面是要被拦截的。如果不作此判断当recyclerview里面第一个参数不可见,此时顶部的放大缩小的图片同样是不可见的,你滑动的时候会发现滑动不了,因为它被拦截了
(2)
private void pullAnimator(View headerView, int headerHeight, int headerWidth, int offsitY, int maxHeight) {
if (headerView == null) {
return;
}
int pullOffset = (int) Math.pow(offsitY, 0.8);//为了模拟出类似阻尼的效果,就是offsitY的0.8次幂
int newHeight = Math.min(maxHeight + headerHeight, pullOffset + headerHeight);
int newWidth = (int)(((((float)newHeight / headerHeight))) * headerWidth);//算出宽度增加的量
log(((float)(newHeight / headerHeight))+"");
headerView.getLayoutParams().width = newWidth;
headerView.getLayoutParams().height = newHeight;
// //如果不要下面这2行,你运行代码,你会发现图片左边的白边部分会更大(在拖动期间)
int margin = (newWidth - headerWidth) / 2;
headerView.setTranslationX( -margin);
headerView.requestLayout();
}
Math.pow是为了模拟出阻尼的效果才写上的,还有一点要注意(float)newHeight / headerHeight,是要先把newHeight先转为float,我开始模仿写的时候,把float写在外边了,导致算出的newHeight和headerWidth是相等的。
headerView.setTranslationX( -margin)是为了解决图片会像右边拉伸,这样就不会达到我们的预期了
第二个学习总结
在下拉的时候,小圆圈从上而下的显示出来
第一步:设置小圆圈的位置
第二步:下拉的时候滑出小圆圈
第三步:释放小圆圈回去
第一步代码:
public xuexiScrollFlexibleLayout setRefreshView(View refreshViewm, OnRefreshListener listener) {
if (mRefreshView != null) {
removeView(mRefreshView);
}
mRefreshView = refreshViewm;
mRefreshListener = listener;
FrameLayout.LayoutParams layoutParams = new LayoutParams(mRefreshSize, mRefreshSize);
layoutParams.gravity = Gravity.CENTER_HORIZONTAL;
mRefreshView.setLayoutParams(layoutParams);
mRefreshView.setTranslationY(-mRefreshSize);
addView(mRefreshView);
return this;
}
这里通过设置setTranslationY来定义了Y轴的位置,这样就会被隐藏起来,也设置了方向是水平居中
第二步代码:
首先被onInterceptTouchEvent拦截了事件都交要给touchEvent处理
public void changeRefreshView(int offsetY) {
if (!isRefreshable || mRefreshView == null || isRefreshing()){
return;
}
int pullOffset = (int) Math.pow(offsetY, 0.9);
int newHeight = Math.min(mMaxRefreshPullHeight, pullOffset);
//这里定义了可以下拉的最大的高度了
mRefreshView.setTranslationY(-mRefreshSize + newHeight);
mRefreshView.setRotation(pullOffset);
mRefreshView.requestLayout();
}
Math.pow方法主要是模仿出阻尼效果。 mRefreshView.setTranslationY(-mRefreshSize + newHeight);
首先newHeight的高度是从小慢慢变大的,所以小圆圈的位置会慢慢从看不见转移到看见小圆圈,当然了你移动了位置需要requestLayout()刷新
setRotation,表示围绕Z轴旋转,我黏贴一张图出来
图片来源地址:View的平移、缩放、旋转以及位置、坐标系
第三步代码:
public void changeRefreshViewOnActionUp(int offsetY) {
if (!isRefreshable || isRefreshing())
return;
mIsRefreshing = true;
//如果下拉的位移>给定的最大下拉高度,就继续旋转
if (offsetY > mMaxRefreshPullHeight){
float rotation = mRefreshView.getRotation();
mRefreshingAnimator = ObjectAnimator.ofFloat(mRefreshView,"rotation",rotation,rotation+360);
mRefreshingAnimator.setDuration(1000);
mRefreshingAnimator.setInterpolator(new LinearInterpolator());
mRefreshingAnimator.setRepeatMode(ValueAnimator.RESTART);
mRefreshingAnimator.setRepeatCount(-1);
mRefreshingAnimator.start();
if (mRefreshListener != null) {
mRefreshListener.onRefreshing();
}
}else {// 下拉的位移<可允许下拉的最大高度
if (mRefreshingAnimator!=null){
mRefreshingAnimator.cancel();
}
float translation = mRefreshView.getTranslationY();
//Y轴上要回到原始位置,所以先获取现在的位置然后,回到原来的位置
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(mRefreshView,"translationY",translation,-mRefreshSize);
objectAnimator.setDuration(500);
objectAnimator.setInterpolator(new LinearInterpolator());
objectAnimator.addListener(mRefreshAnimatorListener);
objectAnimator.start();
}
}
第二章学习文章地址: Android 自定义弧形旋转菜单栏——卫星菜单
知识摘要
private void startOpenAnim() {
int count = mMenuItemResIds.size();
List animators = new ArrayList<>();
for (int i = 0; i < count; i++) {
//知识点:Math.sin(double d);参数d为弧度值,需要将度数转换成弧度值
int tranX = -(int) (mRadius * Math.sin(Math.toRadians(90 * i / (count - 1))));
int tranY = -(int) (mRadius * Math.cos(Math.toRadians(90 * i / (count - 1))));
ObjectAnimator animatorX = ObjectAnimator.ofFloat(mImgViews.get(i), "translationX", 0f, tranX);
ObjectAnimator animatorY = ObjectAnimator.ofFloat(mImgViews.get(i), "translationY", 0f, tranY);
ObjectAnimator alpha = ObjectAnimator.ofFloat(mImgViews.get(i), "alpha", 0, 1);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(mImgViews.get(i), "scaleX", 0.1f, 1);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(mImgViews.get(i), "scaleY", 0.1f, 1);
animators.add(animatorX);
animators.add(animatorY);
animators.add(alpha);
animators.add(scaleX);
animators.add(scaleY);
}
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(mDuration);
animatorSet.playTogether(animators);
animatorSet.start();
rotateMenu(0, 90);
}
Math.sin(Math.toRadians(90 * i / (count - 1):原来Math.sin(double d)方法里面不是填写角度,而是填写弧度值,Math.toRadians就是计算弧度值的方法
第三章学习地址:Android 自定义带入场动画的弧形百分比进度条
知识点摘要
private void setAnimprogressTime(float start,float endValue) {
//知识点ValueAnimator.ofFloat(start, endValue);
ValueAnimator animator = ValueAnimator.ofFloat(start, endValue);
animator.setDuration(animTime);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fav = (float) animation.getAnimatedValue();
progressValue = (int)fav;
invalidate();
}
});
animator.start();
}
我最开始自己写的时候是这样写的
ValueAnimator animator = ValueAnimator.ofFloat(start, progressValue );
这里progressValue 是当前进度条的值且我在onAnimationUpdate还一直赋值给我progressValue ,结果我这样写导致我无法进入到页面,手机一片黑
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int center = getWidth() / 2;
float radius = center - roundWidth / 2;
/**
* 绘制后面的整圆
*/
paint.setStyle(Paint.Style.STROKE); //设置空心
paint.setStrokeWidth(bgStrokeWidth); //设置圆环的宽度
paint.setColor(roundColor);
paint.setAntiAlias(true); //消除锯齿
canvas.drawCircle(center, center, radius, paint);
/**
* 画进度百分比
*/
paint.setStrokeWidth(0);
paint.setColor(textColor);
paint.setTextSize(textSize);
paint.setTypeface(Typeface.DEFAULT);
if (!TextUtils.isEmpty(centerText)) {
//如果是设置文本内容,则直接测量文本长度并绘制
float textWidth = paint.measureText(centerText);
canvas.drawText(centerText, center - textWidth / 2, center + textSize / 2, paint); //画出进度百分比
} else {
//如果是设置百分比,则计算百分比并绘制
int percent = (int) (((float) progressValue / (float) maxValue) * 100); //中间的进度百分比,先转换成float在进行除法运算,不然都为0
float textWidth = paint.measureText(percent + "%"); //测量字体宽度,我们需要根据字体的宽度设置在圆环中间
if (percent != 0) {
canvas.drawText(percent + "%", center - textWidth / 2, center + textSize / 2, paint); //画出进度百分比
}
}
paint.setStrokeWidth(progressStrokeWidth);
paint.setColor(progressColor);
paint.setStrokeCap(Paint.Cap.ROUND);//知识点:画线端是否带有圆角
mArcRectF.left = center - radius;
mArcRectF.top = center - radius;
mArcRectF.right = center + radius;
mArcRectF.bottom = center + radius;
paint.setStyle(Paint.Style.STROKE);
//知识点
canvas.drawArc(mArcRectF, 90 - 180 * ((float) progressValue / (float) maxValue), 360 * progressValue / maxValue, false, paint); //根据进度画圆弧
}
我一直思考,为啥他可以从底部中间开始,慢慢像2变画弧线呢?这个问题我是这样想的
开始角度:90 - 180 * ((float) progressValue / (float) maxValue)
弧度值:360 * progressValue / maxValue,这个很好理解,占有百分比嘛
解释:
首先要明白drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter,)里面的startAngle,sweepAngle。
startAngle:表示开始的角度
sweepAngle:sweepAngle划过的弧度
代码里面我们一直有调用invalidate(),所以ondrow()方法一直会调用。无论如何progressValue 都是先从0开始变化到40。
假如现在 progressValue = 0 maxValue = 100;红色的那根实线与X轴的夹角为90°弧度值=0,画弧线是按顺时针的方向,随着时间的流逝与X轴的夹角会慢慢变小最后会形成如上的效果,弧度值占整个360°的百分比也会慢慢变大,假如此时的progressValue = 29,下一秒就是30,那么实线红色的高度会比之前高一点点,弧度值会比之前大一点点,然后就真的实现了作者的那种往2边走的效果了(其实是你眼睛欺骗了你)
学习制作雷达图
Android Path 最佳实践之绘制雷达图
做出这个图,让我断断续续的做了几天才弄出来。
这里我想分3个步骤来做
1、画出网格
2、往上面标记文字
3、区域渲染
第一步骤画出网格
//重要,我这里将画布迁移到屏幕中间,这样我们在计算的时候,不需要+/-
canvas.translate(centerX, centerY);
Path path = new Path();
Path linePath = new Path();
float r = radius / (count - 1);//每个边边的半径
for (int i = 0; i < count; i++) {
float curR = r * i;
path.reset();
linePath.reset();
for (int j = 0; j < count; j++) {
//绘制交叉线
linePath.moveTo(0, 0);
linePath.lineTo((float) (curR * Math.cos(angle * j)), (float) (curR * Math.sin(angle * j)));
canvas.drawPath(linePath, paint);
//开始绘制单个多边型
if (j == 0) {
path.moveTo(curR, 0);
} else {
path.lineTo((float) (curR * Math.cos(angle * j)), (float) (curR * Math.sin(angle * j)));
}
}
path.close();
canvas.drawPath(path, paint);
}
首先这里有5个多边行,我们绘制应该从最小的那个多变行绘制开始,我们用大的半径(radius)/个数,算出平均绘制一个的半径是多大。
path.reset();linePath.reset();主要目的是为了每次画完一个6边行,就重置一下。
绘制交叉线,我们首先应该把笔放到圆心处,然后就开始绘制直线,注意使用moveTo不会导致线条是连起来的,lineTo会导致线条连起来
第二步骤绘制文字
double pi = Math.PI;
Paint textPaint = new Paint();
textPaint.setTextSize(35);
Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
float textHeight = fontMetrics.descent - fontMetrics.ascent;
float textRadius = radius + textHeight;
for (int i = 0; i < arrayList.size(); i++) {
float dgress = angle * i;
float x = (float) (textRadius * Math.cos(dgress));
float y = (float) (textRadius * Math.sin(dgress));
if (pi / 2 >= dgress && dgress >= 0) {//第四象限
float dist = textPaint.measureText(arrayList.get(i)) / (arrayList.get(i).length());
canvas.drawText(arrayList.get(i), x-30 , y, textPaint);
} else if (dgress > pi / 2 && dgress <= 2 * pi) {//第三象限
float dist = textPaint.measureText(arrayList.get(i)) / (arrayList.get(i).length());
canvas.drawText(arrayList.get(i), x - dist, y, textPaint);
}else if (dgress > 2 * pi && dgress <= pi/2 * 3){//第二象限
float dist = textPaint.measureText(arrayList.get(i)) / (arrayList.get(i).length() - 1);
canvas.drawText(arrayList.get(i), x - dist, y, textPaint);
}else {//第一象限
float dist = textPaint.measureText(arrayList.get(i)) / (arrayList.get(i).length() - 1);
canvas.drawText(arrayList.get(i), x + dist, y, textPaint);
}
}
这里的第一,二,三,四象限,是这样划分的,我们将画布移动到中间后,X,Y轴正方向是第一象限
第三步,绘制覆盖区域
Paint fugaiPain = new Paint();
fugaiPain.setStyle(Paint.Style.FILL_AND_STROKE);
fugaiPain.setColor(Color.RED);
fugaiPain.setAlpha(200);
Path fugaiPath = new Path();
for (int i = 0; i < integerList.size(); i++) {
float currentScorePercnter = (Float) integerList.get(i) / 100f;
float currentRadius = currentScorePercnter * radius;
float x = (float) (currentRadius * Math.cos(angle * i));
float y = (float) (currentRadius * Math.sin(angle * i));
canvas.drawCircle(x,y,10,fugaiPain);
if (i==0){
fugaiPath.moveTo(x,y);
}else {
fugaiPath.lineTo(x,y);
}
}
fugaiPath.close();
fugaiPain.setAlpha(100);
canvas.drawPath(fugaiPath,fugaiPain);
//全部代码
public class SixBorderView extends View {
private Paint paint;
private int centerX, centerY;
private float radius;//半径
private float angle;//角度
private int count = 6;//绘制边的个数
private List arrayList = new ArrayList<>();
private List integerList = new ArrayList<>();
public SixBorderView(Context context) {
this(context, null);
}
public SixBorderView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SixBorderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paint = new Paint();
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.STROKE);
arrayList.add("物理");
integerList.add(30f);
arrayList.add("化学");
integerList.add(60f);
arrayList.add("生物");
integerList.add(80f);
arrayList.add("语文");
integerList.add(50f);
arrayList.add("地理");
integerList.add(45f);
arrayList.add("数学");
integerList.add(38f);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
centerX = w / 2;
centerY = h / 2;
radius = Math.min(centerX, centerY) * 0.8f;
angle = (float) (2 * Math.PI / count);
postInvalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(centerX, centerY);
//1、绘制出网格
Path path = new Path();
Path linePath = new Path();
float r = radius / (count - 1);//每个边边的半径
for (int i = 0; i < count; i++) {
float curR = r * i;
path.reset();
linePath.reset();
for (int j = 0; j < count; j++) {
//绘制交叉线
linePath.moveTo(0, 0);
linePath.lineTo((float) (curR * Math.cos(angle * j)), (float) (curR * Math.sin(angle * j)));
canvas.drawPath(linePath, paint);
//开始绘制单个多边型
if (j == 0) {
path.moveTo(curR, 0);
} else {
path.lineTo((float) (curR * Math.cos(angle * j)), (float) (curR * Math.sin(angle * j)));
}
}
path.close();
canvas.drawPath(path, paint);
}
//2、绘制文字
double pi = Math.PI;
Paint textPaint = new Paint();
textPaint.setTextSize(35);
Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
float textHeight = fontMetrics.descent - fontMetrics.ascent;
float textRadius = radius + textHeight;
for (int i = 0; i < arrayList.size(); i++) {
float dgress = angle * i;
float x = (float) (textRadius * Math.cos(dgress));
float y = (float) (textRadius * Math.sin(dgress));
if (pi / 2 >= dgress && dgress >= 0) {//第四象限
float dist = textPaint.measureText(arrayList.get(i)) / (arrayList.get(i).length());
canvas.drawText(arrayList.get(i), x-30 , y, textPaint);
} else if (dgress > pi / 2 && dgress <= 2 * pi) {//第三象限
float dist = textPaint.measureText(arrayList.get(i)) / (arrayList.get(i).length());
canvas.drawText(arrayList.get(i), x - dist, y, textPaint);
}else if (dgress > 2 * pi && dgress <= pi/2 * 3){//第二象限
float dist = textPaint.measureText(arrayList.get(i)) / (arrayList.get(i).length() - 1);
canvas.drawText(arrayList.get(i), x - dist, y, textPaint);
}else {//第一象限
float dist = textPaint.measureText(arrayList.get(i)) / (arrayList.get(i).length() - 1);
canvas.drawText(arrayList.get(i), x + dist, y, textPaint);
}
}
//3、绘制覆盖区域
Paint fugaiPain = new Paint();
fugaiPain.setStyle(Paint.Style.FILL_AND_STROKE);
fugaiPain.setColor(Color.RED);
fugaiPain.setAlpha(200);
Path fugaiPath = new Path();
for (int i = 0; i < integerList.size(); i++) {
float currentScorePercnter = (Float) integerList.get(i) / 100f;
float currentRadius = currentScorePercnter * radius;
float x = (float) (currentRadius * Math.cos(angle * i));
float y = (float) (currentRadius * Math.sin(angle * i));
canvas.drawCircle(x,y,10,fugaiPain);
if (i==0){
fugaiPath.moveTo(x,y);
}else {
fugaiPath.lineTo(x,y);
}
}
fugaiPath.close();
fugaiPain.setAlpha(100);
canvas.drawPath(fugaiPath,fugaiPain);
}
}
学习实现流动标签布局
学习地址
特别需要注意的点
1、onMeasure会被多次测量,主要是因为performTranversals的原因导致的
public class TagGroupView extends ViewGroup {
private final String TAG = "TagGroupView";
private int customInterval = 15;
private int customSonPaddingLeft = 20;
private int customSonPaddingRight = 20;
private int customSonPaddingTop = 10;
private int customSonPaddingBottom = 10;
private Drawable customSonBackground = null;
private Drawable customSonChoseBackground = null;
private int customSonTextColor;
private float customSonTextSize = 0;
private ArrayList mSonTextContents = new ArrayList<>();
private ArrayList mSonTextViews = new ArrayList<>();
private Context mContext = null;
private OnItemClickListener mOnItemClickListener;
private int customSelectMode;//101单选 102多选
/**
* 设置标签点击事件
*
* @param onItemClickListener 回调接口
*/
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
this.mOnItemClickListener = onItemClickListener;
}
public interface OnItemClickListener {
void onItemClick(View view, int position, String sonContent);
}
public TagGroupView(Context context) {
this(context,null);
}
public TagGroupView(Context context, AttributeSet attrs) {
this(context, attrs,0);
initAttrs(context,attrs);
}
public TagGroupView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
//初始化自定义属性
initAttrs(context,attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int mParentMearWidht = MeasureSpec.getSize(widthMeasureSpec);
int line = 1;
int currentWidth = customInterval;//子view的宽度;
int currentHeight = 0;//子view的高度
if (mSonTextContents.size() > 0){
mSonTextViews.clear();
for (int i = 0; i < mSonTextContents.size(); i++) {
TextView textView = getSonTextView(i,mSonTextContents.get(i));
//获取TextView的宽高
textView.measure(0,0);
currentHeight = textView.getMeasuredHeight() + customInterval + customInterval;
int textViewWidht = textView.getMeasuredWidth() + customInterval;
//判断是否可以在一行显示,如果显示不了,需要换行
if (mParentMearWidht - currentWidth >= textViewWidht){
currentWidth = currentWidth + textViewWidht;
}else {
line = line + 1;
currentWidth = customInterval;
}
}
}
setMeasuredDimension(mParentMearWidht,currentHeight * line );
}
private TextView getSonTextView(int i,final String content){
TextView textView = new TextView(mContext);
textView.setPadding(customSonPaddingLeft,customSonPaddingTop,customSonPaddingRight,customSonPaddingBottom);
textView.setTextColor(customSonTextColor);
textView.setText(content);
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
//去掉默认的内边距
textView.setIncludeFontPadding(false);
textView.setBackground(customSonBackground);
textView.setTag(i);
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
int i = (int) v.getTag();
TextView textView1 = mSonTextViews.get(i);
if (customSelectMode==101){//单选
for (TextView mSonTextView : mSonTextViews) {
mSonTextView.setSelected(false);
mSonTextView.setBackground(customSonBackground);
}
textView1.setSelected(true);
}else {//多选
if (textView1.isSelected()){
textView1.setSelected(false);
}else {
textView1.setSelected(true);
}
}
if (textView1.isSelected()){
textView1.setBackground(customSonChoseBackground);
}else {
textView1.setBackground(customSonBackground);
}
if (mOnItemClickListener!=null){
mOnItemClickListener.onItemClick(v,i,content);
}
}
});
mSonTextViews.add(textView);
return textView;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//定义left,top的默认高度
int left = customInterval;
int top = customInterval;
//整个View的宽度
int mParentWidth = this.getMeasuredWidth();
//移除重复的
this.removeAllViews();
//循环遍历,取出TextView,将TextView添加到里面去
for (int i = 0; i < mSonTextViews.size(); i++) {
TextView textView = mSonTextViews.get(i);
this.addView(textView);
//获取子View的宽高,判断一行是否可以放的下,如果放不下,需要换行
int childHeight = textView.getMeasuredHeight() + customInterval;
int childWidth = textView.getMeasuredWidth() + customInterval;
if ((mParentWidth - left) >= childWidth){
textView.layout(left,top,left + childWidth,top + childHeight);
left = left + childWidth + customInterval;
}else{
left = customInterval;
top = top + childHeight +customInterval;
textView.layout(left,top,left + childWidth,top + childHeight);
}
}
}
/**
* 初始化TypeArray参数
* @param context
* @param attrs
*/
private void initAttrs(Context context,AttributeSet attrs){
TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.TagGroupView);
customSonBackground = mTypedArray.getDrawable(R.styleable.TagGroupView_customSonBackground);
customSonChoseBackground = mTypedArray.getDrawable(R.styleable.TagGroupView_customSonChoseBackground);
if (customSonBackground==null){
customSonBackground = ContextCompat.getDrawable(context,R.drawable.rectangle_b_radious5_gray_solid_white);
}
if (customSonChoseBackground==null){
customSonChoseBackground = ContextCompat.getDrawable(context,R.drawable.rectangle_b_radious5_red_solid_white);
}
customInterval = (int) mTypedArray.getDimension(R.styleable.TagGroupView_customInterval, customInterval);
customSonPaddingLeft = (int) mTypedArray.getDimension(R.styleable.TagGroupView_customSonPaddingLeft, customSonPaddingLeft);
customSonPaddingRight = (int) mTypedArray.getDimension(R.styleable.TagGroupView_customSonPaddingRight, customSonPaddingRight);
customSonPaddingTop = (int) mTypedArray.getDimension(R.styleable.TagGroupView_customSonPaddingTop, customSonPaddingTop);
customSonPaddingBottom = (int) mTypedArray.getDimension(R.styleable.TagGroupView_customSonPaddingBottom, customSonPaddingBottom);
customSonTextSize = (int) mTypedArray.getDimension(R.styleable.TagGroupView_customSonTextSize, 0);
customSonTextColor = mTypedArray.getColor(R.styleable.TagGroupView_customSonTextColor,getResources().getColor(R.color.blue));
customSelectMode = mTypedArray.getInt(R.styleable.TagGroupView_customSelectMode, 101);
if (customSonTextSize == 0) {
customSonTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12, getResources().getDisplayMetrics());
}
mTypedArray.recycle();
}
/**
* 设置标签内容集合
*
* @param sonContent 标签内容
*/
public void setSonContent(List sonContent) {
if (sonContent != null) {
mSonTextContents.clear();
mSonTextContents.addAll(sonContent);
requestLayout();
}
}
/**
* 添加一个标签
*
* @param sonContent 标签内容
*/
public void addSonContent(String sonContent) {
mSonTextContents.add(0, sonContent);
requestLayout();
}
}
//为了图方便,我就直接放这里了
public class TagActivity extends Activity {
private TagGroupView tagGroupView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tag);
tagGroupView = findViewById(R.id.tag_taggroupview);
List stringList = new ArrayList<>();
for (int i=0;i<30;i++){
stringList.add("德玛西亚"+i);
}
tagGroupView.setSonContent(stringList);
}
}
xml
声音录制
学习文章地址:https://www.jianshu.com/p/6dd10a5adca8
实现这个很简单,只要掌握了底部下标的数字根据柱状图的个数变化规律就行
首先是平分一半,黑色的0,1表示第0位,第1位,红色的表示是一个等比数列,0跟2直接相差了2个lineWidth宽度,我们2个矩形直接的宽度是lineWidth,0跟文字之间的距离也是lineWidth。由此我们可以得到公式
偶数:2 * i * lineWidth + lineWidth
奇数:(2 * i + 1) * lineWidth + lineWidth
for (int i = 0; i < 10; i++) {
//左边的柱状图
rectLeft.right = parentWidth - textWidth - 2 * i * lineWidth - lineWidth;
rectLeft.left = parentWidth - textWidth - (2 * i +1) * lineWidth - lineWidth;
rectLeft.top = parentHeight - mWaveList.get(i) * lineWidth / 2;
rectLeft.bottom = parentHeight + mWaveList.get(i) * lineWidth / 2;
//右边的柱状图
rectRight.left =parentWidth + textWidth + 2 * i * lineWidth + lineWidth;
rectRight.right = parentWidth + textWidth + (2 * i +1) * lineWidth + lineWidth;
rectRight.top = parentHeight - mWaveList.get(i) * lineWidth / 2;
rectRight.bottom = parentHeight + mWaveList.get(i) * lineWidth / 2;
canvas.drawRect(rectLeft,paint);
canvas.drawRect(rectRight,paint);
}
心得
1、我们画矩形,先从左边开始画起,然后再画右边。
2、如果我们定义的这个标签宽度不够宽,但是绘画的宽度比我们定义的宽的话,会导致只能显示部分
LineWaveView lineWaveView = findViewById(R.id.linwave_line_wave_voice);
lineWaveView.startRecord();
/**
* @date: 2019/5/13 0013
* @author: gaoxiaoxiong
* @description:自定义声音View
**/
public class LineWaveView extends View {
private static final String DEFAULT_TEXT = " 请录音 ";
private static final int LINE_WIDTH = 9;//默认矩形波纹的宽度,9像素, 原则上从layout的attr获得
private Paint paint = new Paint();
private Runnable task;
private RectF rectRight = new RectF();
private RectF rectLeft = new RectF();
private String text = DEFAULT_TEXT;
private int updateSpeed;
private int lineColor;
private int textColor;
private float lineWidth = LINE_WIDTH;
private float textSize;
private Context mContext;
private static final int MIN_WAVE_HEIGHT = 2;//矩形线最小高
private static final int MAX_WAVE_HEIGHT = 12;//矩形线最大高
private static final int[] DEFAULT_WAVE_HEIGHT = {2, 2, 2, 2, 2, 2, 2, 2, 2, 2,2};
private static final int UPDATE_INTERVAL_TIME = 100;//100ms更新一次
private LinkedList mWaveList = new LinkedList<>();
private float maxDb;
public boolean isStart = false;
public LineWaveView(Context context) {
this(context, null);
this.mContext = context;
}
public LineWaveView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
this.mContext = context;
}
public LineWaveView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LineWaveView);
lineColor = typedArray.getColor(R.styleable.LineWaveView_lwvLineColor, context.getResources().getColor(R.color.blueColorPrimary));
lineWidth = typedArray.getDimension(R.styleable.LineWaveView_lwvLineWidth, LINE_WIDTH);
textSize = typedArray.getDimension(R.styleable.LineWaveView_lwvTextSize, context.getResources().getDimensionPixelSize(R.dimen.text_size_14));
textColor = typedArray.getColor(R.styleable.LineWaveView_lwvTextColor, context.getResources().getColor(R.color.black_000000));
updateSpeed = typedArray.getDimensionPixelSize(R.styleable.LineWaveView_lwvUpdateSpeed, UPDATE_INTERVAL_TIME);
typedArray.recycle();
//设置默认的值
resetView(mWaveList, DEFAULT_WAVE_HEIGHT);
}
private synchronized void refreshElement() {
Random random = new Random();
maxDb = random.nextInt(5) + 2;
int waveH = MIN_WAVE_HEIGHT + Math.round(maxDb * (MAX_WAVE_HEIGHT - MIN_WAVE_HEIGHT));
mWaveList.add(0, waveH);
mWaveList.removeLast();
}
private class MyRunnerTask implements Runnable{
@Override
public void run() {
while (isStart) {
refreshElement();
try {
Thread.sleep(updateSpeed);
} catch (InterruptedException e) {
e.printStackTrace();
}
postInvalidate();
}
}
}
/**
* @date: 2019/5/27 0027
* @author: gaoxiaoxiong
* @description:开始任务
**/
public synchronized void startRecord() {
isStart = true;
new Thread(new MyRunnerTask(),"线程").start();
}
/**
* @date: 2019/5/27 0027
* @author: gaoxiaoxiong
* @description:停止任务
**/
public synchronized void stopRecord() {
isStart = false;
mWaveList.clear();
resetView(mWaveList, DEFAULT_WAVE_HEIGHT);
postInvalidate();
}
private void resetView(List list, int[] array) {
list.clear();
for (int anArray : array) {
list.add(anArray);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int parentWidth = getWidth() / 2;
int parentHeight = getHeight() / 2;
paint.setColor(textColor);
paint.setTextSize(textSize);
paint.setStrokeWidth(0);
float textWidth = paint.measureText(text) / 2;
canvas.drawText(text, parentWidth - textWidth, parentHeight - (paint.ascent() + paint.descent()) / 2, paint);
paint.setColor(lineColor);
paint.setStrokeWidth(lineWidth);
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
for (int i = 0; i < 10; i++) {
//左边的柱状图
rectLeft.right = parentWidth - textWidth - 2 * i * lineWidth - lineWidth;
rectLeft.left = parentWidth - textWidth - (2 * i +1) * lineWidth - lineWidth;
rectLeft.top = parentHeight - mWaveList.get(i) * lineWidth / 2;
rectLeft.bottom = parentHeight + mWaveList.get(i) * lineWidth / 2;
//右边的柱状图
rectRight.left =parentWidth + textWidth + 2 * i * lineWidth + lineWidth;
rectRight.right = parentWidth + textWidth + (2 * i +1) * lineWidth + lineWidth;
rectRight.top = parentHeight - mWaveList.get(i) * lineWidth / 2;
rectRight.bottom = parentHeight + mWaveList.get(i) * lineWidth / 2;
canvas.drawRect(rectLeft,paint);
canvas.drawRect(rectRight,paint);
}
}
}
8、拖动控件到随意位置
学习地址 Android之View拖拽效果
说明:
原作者没有像我这样可以在空白的地方继续的拖动,我进行了一波完善
心得:
1、需要拖动的控件需要使用startDragAndDrop方法,该方法含有4个参数
分别是ClipData,DragShadowBuilder,Object,flags。ClipData一般我们去实现复制文本到剪切板的时候用到,DragShadowBuilder是生成一个虚拟的影像,Object是要拖动的View,flags我们传递0即可
2、setOnDragListener方法,我的白色的是一个Relayout,我想监听按钮,那么我就必须去实现他,且我return的值必须是true,否则我无法监听到我拖动View的具体位置
按钮长按监听
dragView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
dragView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
View.DragShadowBuilder builder = new View.DragShadowBuilder(dragView);
// 剪切板数据,可以在DragEvent.ACTION_DROP方法的时候获取。
ClipData data = ClipData.newPlainText("Label", "我是文本内容!");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//参与拖动
dragView.startDragAndDrop(data, builder, dragView, 0);
} else {
dragView.startDrag(data, builder, dragView, 0);
}
return true;
}
});
白色部分监听,蓝色部分监听
//白色部分
relativeLayout.setOnDragListener(new View.OnDragListener() {
@Override
public boolean onDrag(View v, DragEvent event) {
switch (event.getAction()) {
case DragEvent.ACTION_DROP://释放拖拽的view
float x = event.getX();
float y = event.getY();
DragView dragView = (DragView) event.getLocalState();
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.topMargin = (int) (y - dragView.getHeight() / 2);
layoutParams.leftMargin = (int) (x - dragView.getWidth() / 2);
if (dragView.getParent() instanceof LinearLayout){
((LinearLayout)dragView.getParent()).removeView(dragView);
relativeLayout.addView(dragView,layoutParams);
}else {
dragView.setLayoutParams(layoutParams);
}
break;
}
return true;
}
});
//蓝色部分
ll_dragview.setOnDragListener(new View.OnDragListener() {
@Override
public boolean onDrag(View v, DragEvent event) {
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
Log.i(TAG, "开始拖拽");
break;
case DragEvent.ACTION_DRAG_ENDED:
Log.i(TAG, "结束拖拽");
break;
case DragEvent.ACTION_DRAG_ENTERED:
Log.i(TAG, "拖拽的view进入监听的view时");
break;
case DragEvent.ACTION_DRAG_EXITED:
Log.i(TAG, "拖拽的view离开监听的view时");
ll_dragview.setBackgroundColor(Color.parseColor("#3F51B5"));
break;
case DragEvent.ACTION_DRAG_LOCATION://拖拽的view在监听view中的位置
break;
case DragEvent.ACTION_DROP://释放拖拽的view
float x = event.getX();
float y = event.getY();
Log.i(TAG, "释放拖拽的view:x =" + x + ",y=" + y);
DragView dragView = (DragView) event.getLocalState();
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.topMargin = (int) (y - dragView.getHeight() / 2);
layoutParams.leftMargin = (int) (x - dragView.getWidth() / 2);
if (dragView.getParent() instanceof RelativeLayout){
((RelativeLayout)dragView.getParent()).removeView(dragView);
ll_dragview.addView(dragView,layoutParams);
}else {
dragView.setLayoutParams(layoutParams);
}
break;
}
return true;
}
});
9、模仿ViewPager
学习地址:
Android Scroller完全解析,关于Scroller你所需知道的一切
android 布局之滑动探究 scrollTo 和 scrollBy 方法使用说明
学习到的知识点
1、event.getRawX()是什么?event.getRawX()表示手指在每一个控件上距离屏幕边缘的距离
2、getScrollX()会一直叠加么?会一直叠加的,比如现在有3个,你拖动第一个,到第二个,到第三个,数值会增大的
3、有了getScrollX(),为啥还要mXLastMove - mXMove,mXLastMove - mXMove表示跟上次滑动的差距,都是在每一个控件上面滑动的距离
4、为啥要做 getScrollX() + getWidth() + scrolledX > rightBorder的判断,直接getScrollX() + scrolledX 不好么?我们滑动的时候已经滑动到了最后一个了getScrollX()是前2个已经滚动完后的距离了,所以 + getWidth() 如果还可以继续滚动的话,就会滚动出去了
public class ScrollerLayout extends ViewGroup {
private String TAG = ScrollerLayout.class.getSimpleName();
/**
* 用于完成滚动操作的实例
*/
private Scroller mScroller;
/**
* 判定为拖动的最小移动像素数
*/
private int mTouchSlop;
/**
* 手机按下时的屏幕坐标
*/
private float mXDown;
/**
* 手机当时所处的屏幕坐标
*/
private float mXMove;
/**
* 上次触发ACTION_MOVE事件时的屏幕坐标
*/
private float mXLastMove;
/**
* 界面可滚动的左边界
*/
private int leftBorder;
/**
* 界面可滚动的右边界
*/
private int rightBorder;
public ScrollerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
ViewConfiguration configuration = ViewConfiguration.get(context);
//拖动的最小移动像素数
mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
leftBorder = getChildAt(0).getLeft();
rightBorder = getChildAt(childCount - 1).getRight();
Log.i(TAG,"rightBorder:"+rightBorder);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
mXDown = ev.getRawX();
mXLastMove = mXDown;
}
break;
case MotionEvent.ACTION_MOVE: {
mXMove = ev.getRawX();
float diff = Math.abs(mXMove - mXDown);
mXLastMove = mXMove;
if (diff > mTouchSlop) {
return true;
}
}
break;
}
return super.onInterceptTouchEvent(ev);
}
//问题1:getScrollX()会一直叠加么? 会一直叠加的,比如现在有3个,每一个的位置都是固定的,你拖动第一个,其它的2个都会跟着动
//问题2:有了getScrollX(),为啥还要mXLastMove - mXMove,mXLastMove - mXMove表示跟上次滑动的差距,都是在每一个控件上面滑动的距离
//问题3:event.getRawX()是什么?event.getRawX()表示手指在每一个控件上距离屏幕边缘的距离
//问题4:为啥要做 getScrollX() + getWidth() + scrolledX > rightBorder的判断,直接getScrollX() + scrolledX 不好么?我们滑动的时候已经滑动到了最后一个了getScrollX()是前2个已经滚动完后的距离了,所以 + getWidth() 如果还可以继续滚动的话,就会滚动出去了
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE: {
mXMove = event.getRawX();
int scrolledX = (int) (mXLastMove - mXMove);
Log.i(TAG,"mXMove:"+mXMove);
//Log.i(TAG,"getScrollX():"+getScrollX());
if (getScrollX() + scrolledX < leftBorder) {
scrollTo(leftBorder, 0);
return true;
} else if (getScrollX() + getWidth() + scrolledX > rightBorder) {
scrollTo(rightBorder - getWidth(), 0);
return true;
}
scrollBy(scrolledX, 0);
mXLastMove = mXMove;
}
break;
case MotionEvent.ACTION_UP: {
int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
int dx = targetIndex * getWidth() - getScrollX();
mScroller.startScroll(getScrollX(),0,dx,0);
invalidate();
}
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
// 第三步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
}
9、NestedScrollingParent2
学习地址
Andorid 嵌套滑动机制 NestedScrollingParent2和NestedScrollingChild2 详解
Android嵌套滑动机制实战演练
Android NestedScrolling(嵌套滑动)机制
小结
1、recyclerView嵌套recyclerView,我子recycleView想要滑动,还是得在继承了NestedScrollingParent2里面,通过childRecyclerView.scrollBy帮助我滑动
2、recyclerView嵌套recyclerView,在第一次down下的位置如果是childRecyclerview,那么第一次响应的target就是childRecyclerView,后续的move事件,父recyclerView也会一直响应到
3、速率是矢量,是具有方向的Y轴上,竖直方向从上往下滑动是 正值,从下往上是负值
4、针对MotionEvent.ACTION_MOVE事件里面,永远都是lastX/Y-current