内容简介:这是我曾经测试画出的一张图,描述了ViewGroup+两个孩子的生命函数调用情况经测试发现注意点:这里使用适配器模式,跟ListView一个套路,其实是非常简单,看箭头所指
1.自定义ViewGroup中花样布局子View 2.移动View用layout、translation、TranslationAnimation、ViewPropertyAnimator、scrollTo、scrollBy有什么区别? 3.ViewGroup里怎么给孩子加动画 4.惯性移动? 也许你可以了解一下 复制代码
效果 1 | 效果 2 |
---|---|
一、前置知识:
1.生命函数
这是我曾经测试画出的一张图,描述了ViewGroup+两个孩子的生命函数调用情况
在这补充一点,ViewGroup在没有背景时不会走onDraw方法,但可以走dispatchDraw 原因在于View对onDraw的控制时做了限定:[if (!dirtyOpaque) onDraw(canvas)] 你可以使用onDraw,在之前设个透明色即可:setBackgroundColor(0x00000000); 复制代码
2.View与Activity之间
貌似一直没有对Activity与View的生命周期一起做过测试
测试之后发现View加载完成之后( onFinishInflate
)并未立即回调测量、布局、绘制
onResume
之后View才会回调
onAttachedToWindow-->onMeasure-->onSizeChanged-->onLayout-->onDraw
这一点确实让我挺惊讶,以前竟然没注意,现在理清了,通畅很多
2019-02-19 16:50:29.998 : onCreate -------------- 2019-02-19 16:50:29.992 : 构造函数: 0 2019-02-19 16:50:29.996 : onFinishInflate: 0 2019-02-19 16:50:33.001 : onStart: ................... 2019-02-19 16:50:33.006 : onResume: ................... 2019-02-19 16:50:33.050 : onAttachedToWindow: 2019-02-19 16:50:33.207 : onMeasure: 0 2019-02-19 16:50:33.243 : onMeasure: 0 2019-02-19 16:50:33.354 : onSizeChanged: 1948 2019-02-19 16:50:33.358 : onLayout: 1948 2019-02-19 16:50:33.395 : onDraw: 1948 复制代码
二、自定义ViewGroup ( 排兵布阵
)
经测试发现注意点:
[1].必须onMeasure中测量孩子的尺寸,否则无法显示 [2].必须onLayout中布局孩子的位置,否则无法显示 [3].在onLayout中孩子不能用view.getHeight()获取尺寸(因为为0),只能用view.getMeasuredHeight 复制代码
1.最简形式的ViewGroup
这里使用适配器模式,跟ListView一个套路,其实是非常简单,看箭头所指
这里暂时不对ViewGroup进行测量,先填满。对子View用自带的测量方法 measureChildren
public class FlowerLayout extends ViewGroup { private int mRadius; private static final String TAG = "FlowerLayout"; --->private ListAdapter mAdapter; --->public void setAdapter(ListAdapter adapter) { mAdapter = adapter; } public FlowerLayout(Context context) { this(context, null); } public FlowerLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public FlowerLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(attrs); setBackgroundColor(0x55D3E8FD); } private void init(AttributeSet attrs) { } private void formFlower() { for (int i = 0; i < mAdapter.getCount(); i++) { ---> View petal = mAdapter.getView(i, null, this); addView(petal);//填入花瓣 } } @Override protected void onAttachedToWindow() { Log.e(TAG, "onAttachedToWindow: "); ---> if (mAdapter != null) { formFlower(); } super.onAttachedToWindow(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); measureChildren(widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //TODO 布局子view } } ---->[Activity中使用]---------------- setContentView(R.layout.activity_flower); FlowerLayout flowerLayout = findViewById(R.id.id_fl); ArrayList<Petal> petals = new ArrayList<>(); petals.add(new Petal(R.mipmap.icon_1, "icon_1")); petals.add(new Petal(R.mipmap.icon_2, "icon_2")); petals.add(new Petal(R.mipmap.icon_3, "icon_3")); petals.add(new Petal(R.mipmap.icon_4, "icon_4")); petals.add(new Petal(R.mipmap.icon_5, "icon_5")); petals.add(new Petal(R.mipmap.icon_6, "icon_6")); petals.add(new Petal(R.mipmap.icon_7, "icon_7")); petals.add(new Petal(R.mipmap.icon_8, "icon_8")); petals.add(new Petal(R.mipmap.icon_9, "icon_9")); petals.add(new Petal(R.mipmap.icon_10, "icon_10")); flowerLayout.setAdapter(new FlowerAdapter(petals)); ---->[FlowerAdapter视图适配器]--------------------------- public class FlowerAdapter extends BaseAdapter { private List<Petal> mPetals; public FlowerAdapter(List<Petal> petals) { mPetals = petals; } @Override public int getCount() { return mPetals.size(); } @Override public Object getItem(int position) { return mPetals.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); View view = inflater.inflate(R.layout.flower_item, parent, false); ImageView iv = view.findViewById(R.id.id_pic); iv.setImageResource(mPetals.get(position).resId); TextView tv = view.findViewById(R.id.id_info); tv.setText(mPetals.get(position).info); return view; } } 复制代码
2.布局子view
这里关键在排布这里给张图先:子View布局的左上右下
---->[FlowerLayout#onLayout]---------------- int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); int childW = childView.getMeasuredWidth(); int childH = childView.getMeasuredHeight(); int topPos = (int) (childH * i*0.5f); int leftPos = 0; childView.layout(leftPos, topPos, leftPos + childW, topPos + childH); } |-- 现在只要修改topPos和leftPos就可以改变子View布局 复制代码
3.放置成圆形
---->[FlowerLayout#onLayout]---------------- int count = mAdapter.getCount(); for (int i = 0; i < count; i++) { View childView = getChildAt(i); int childW = childView.getMeasuredWidth(); int childH = childView.getMeasuredHeight(); mRadius = (getWidth()-childW) / 2; float posX = childW / 2 + mRadius - mRadius * cos(i * 360.f / count); float posY = childH / 2 + mRadius - mRadius * sin(i * 360.f / count); int leftPos = (int) (posX - childW / 2); int topPos = (int) (posY - childH / 2); childView.layout(leftPos, topPos, leftPos + childW, topPos + childH); } private float cos(float θ) { return (float) Math.cos(θ / 180 * Math.PI); } private float sin(float θ) { return (float) Math.sin(θ / 180 * Math.PI); } 复制代码
4.添加点击事件
这就比较容易了,一个监听搞定
//在形成View之后就添加点击事件 private void formFlower() { for (int i = 0; i < mAdapter.getCount(); i++) { View petal = mAdapter.getView(i, null, this); int position = i; if (mOnItemClickListener != null) { petal.setOnClickListener(v -> { ObjectAnimator.ofFloat(v, "ScaleX", 1f, 0.8f,1f).setDuration(200).start(); ObjectAnimator.ofFloat(v, "ScaleY", 1f, 0.8f,1f).setDuration(200).start(); mOnItemClickListener.onClick(v, this, position); }); } addView(petal);//填入花瓣 } //----------------------------条目点击监听------------------- public interface OnItemClickListener { void onClick(View v, ViewGroup viewGroup, int position); } private OnItemClickListener mOnItemClickListener; public void setOnItemClickListener(OnItemClickListener onItemClickListener) { mOnItemClickListener = onItemClickListener; } 复制代码
5.数组点阵定位
这突然让我想到一个好玩的东西,那就是点阵控位。
点阵控位可以使用二维数组,也可以使用字符串,也可以使用像素点。
具体的可以详见我的这篇:这里就放一张核心的分析图:我们这里不画圆,而是取点位
/** * 用来显示点阵的二维数组 */ public static final int[][] digit_test = new int[][] { {0, 0, 0, 1, 0, 0, 0}, {0, 0, 1, 0, 1, 0, 0}, {0, 0, 1, 0, 1, 0, 0}, {0, 1, 0, 1, 0, 1, 0}, {0, 1, 0, 0, 0, 1, 0}, {0, 1, 0, 0, 0, 1, 0}, {0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0}, }; /** * 点位解析器 * @param w 单体宽 * @param h 单体高 * @return 解析成的点位数组 */ private List<Point> renderDigit(int w, int h) { List<Point> points = new ArrayList<>(); for (int i = 0; i < digit_test.length; i++) { for (int j = 0; j < digit_test[j].length; j++) {//一行一行遍历,遇到1就画 if (digit_test[i][j] == 1) { int rX = (j * 2 + 1) * (w + 1);//第(i,j)个点圆心横坐标 int rY = (i * 2 + 1) * (h + 1);//第(i,j)个点圆心纵坐标 points.add(new Point(rX, rY)); } } } return points; } ---->[onLayout使用点位]------------------- @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { List<Point> points = renderDigit( getChildAt(0).getMeasuredWidth() / 2, getChildAt(0).getMeasuredHeight() / 2 ); int count = mAdapter.getCount(); for (int i = 0; i < count; i++) { View childView = getChildAt(i); int childW = childView.getMeasuredWidth(); int childH = childView.getMeasuredHeight(); mRadius = (getWidth() - childW) / 2; int leftPos = (int) (points.get(i).x - childW / 2); int topPos = (int) (points.get(i).y - childH / 2); childView.layout(leftPos, topPos, leftPos + childW, topPos + childH); } } 复制代码
ok了,只要把1放在你想要的位置,子View就在那里,
不过简单一点的还好说,要是爱心...来看神技:
5.位图点阵定位
用黑白(就相当于上面1,0)来标识点位,再根据Bitmap的像素进行
Bitmap内存杀手? 7*7像素的Bitmap也就九牛一毛...
就是下面的小不点,你可以下载玩玩。有PS,你也可以用ps自己戳点
mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.heart); /** * 点位解析器 * * @param bitmap bitmap * @param w 单体宽 * @param h 单体高 * @return 解析成的点位数组 */ public static List<Point> renderBitmap(Bitmap bitmap, int w, int h) { List<Point> points = new ArrayList<>(); for (int i = 0; i < bitmap.getWidth(); i++) { for (int j = 0; j < bitmap.getHeight(); j++) { int pixel = bitmap.getPixel(i, j); if (pixel != -1) {//此处过滤掉白颜色 int rX = (i * 2 + 1) * (w + 1);//第(i,j)个点圆心横坐标 int rY = (j * 2 + 1) * (h + 1);//第(i,j)个点圆心纵坐标 points.add(new Point(rX, rY)); } } } return points; } 复制代码
到这里排兵布阵就结束了,相信对onLayout已经能玩的6了吧,接下来上阵杀敌。
二.移动测试篇
既然是测试,就一切从简,直切问题本身,这里新建了一个Activity
并且打开了手机自带的布局便界显示,这样更能说明问题所在
1.布局
自定义:TestViewGroup+TestView纯原生,不加防腐剂
为了说明问题,这里的TestViewGroup加了边距20dp
<?xml version="1.0" encoding="utf-8"?> <com.toly1994.analyzer.widget.TestViewGroup xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/fl" android:layout_marginTop="20dp" android:layout_marginStart="20dp" android:layout_width="300dp" android:layout_height="200dp" android:background="#5597FFFA"> <com.toly1994.analyzer.widget.TestView android:id="@+id/view" android:layout_width="200dp" android:layout_height="50dp" android:background="#23F627"/> </com.toly1994.analyzer.widget.TestViewGroup> 复制代码
2.代码实现
/** * 作者:张风捷特烈<br/> * 时间:2019/2/20/020:10:30<br/> * 邮箱:1981462002@qq.com<br/> * 说明:测试ViewGroup */ public class TestViewGroup extends ViewGroup { public TestViewGroup(Context context) { super(context); } public TestViewGroup(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); measureChildren(widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { View child = getChildAt(0); child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight()); } } /** * 作者:张风捷特烈<br/> * 时间:2019/2/20/020:10:30<br/> * 邮箱:1981462002@qq.com<br/> * 说明:测试View */ public class TestView extends View { public TestView(Context context) { super(context); } public TestView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } } 复制代码
3.移动:layout与translation
这样对比应该非常明显:layout真的把布局移动了,translation只是离家出走而已
layout----- | translation |
---|---|
点击事件在绿色上 | 点击事件在绿色上 |
---->[TestViewGroup#onTouchEvent]------------- @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: int x = (int) event.getX(); int y = (int) event.getY(); // useLayout(mChild, x, y); useTranslation(mChild, x, y); } return super.onTouchEvent(event); } private void useLayout(View view, int x, int y) { view.layout(x, y, x + view.getMeasuredWidth(), y + view.getMeasuredHeight()); //以下四行等价上一行 // mChild.setLeft(x); // mChild.setTop(y); // mChild.setRight(x + mChild.getMeasuredWidth()); // mChild.setBottom(y + mChild.getMeasuredHeight()); } private void useTranslation(View view, int x, int y) { view.setTranslationX(x); view.setTranslationY(y); } 复制代码
4 : 移动动画和属性动画
移动动画都是从家里开始,属性动画移动从当前位置,但是家还在那里!
也就是传说中的属性动画也并无法改变View的布局位置
TranslationAnimation | ViewPropertyAnimator |
---|---|
点击事件在家里 | 点击事件在绿色上 |
private void useTranslationAnimation(View view, int x, int y) { TranslateAnimation translateAnimation = new TranslateAnimation(0, x, 0, y); translateAnimation.setDuration(500); translateAnimation.setFillAfter(true); view.startAnimation(translateAnimation); } private void useViewPropertyAnimator(View view, int x, int y) { view.animate().translationX(x).translationY(y).setDuration(500).start(); //下两句效果同上 // ObjectAnimator.ofFloat(view, "translationX", x).setDuration(500).start(); // ObjectAnimator.ofFloat(view, "translationY", y).setDuration(500).start(); } 复制代码
5:哥就像让View搬家动画怎么办?
很简单:ValueAnimator呗,在刷新时对layout进行更新
由于有四个setXXX方法,这里,简单一点,使用ObjectAnimator
private void useLayoutAnimate(View view, int x, int y) { //下两句效果同上 ObjectAnimator.ofInt(view, "Left", x).setDuration(500).start(); ObjectAnimator.ofInt(view, "Top", y).setDuration(500).start(); ObjectAnimator.ofInt(view, "Right", x+view.getMeasuredWidth()).setDuration(500).start(); ObjectAnimator.ofInt(view, "Bottom", y + view.getMeasuredHeight()).setDuration(500).start(); } 复制代码
6:scrollTo和scrollBy
可以说这两个方法和上面的不是一辈的人,应用场景有很大区别
这两个方法是移动一个View内部的 所有
子View,调用方 并非
子View
至于To和By的区别,也是老生常谈,看图体悟吧...
scrollTo | scrollBy |
---|---|
---->[onTouchEvent]--------------- useScrollTo(-x, -y); useScrollBy(-x, -y); -------------------------------------------- private void useScrollTo(int x, int y) { scrollTo(x, y); } private void useScrollBy(int x, int y) { scrollBy(x, y); } 复制代码
Ok ,基础知识就到这里,言归正传:
三、添加动画
下面这幅图应该不难吧,如果做不出来...下面的就当看风景吧...
静态 | 动态 |
---|---|
1.首先把排成圆的方法封装一下
/** * @param start 第一个排成圆的View索引 * @param dθ 旋转角度 */ private void layoutCircle(int start, float dθ) { int count = getChildCount(); for (int i = start; i < count; i++) { View childView = getChildAt(i); int childW = childView.getMeasuredWidth(); int childH = childView.getMeasuredHeight(); int r = (getWidth() - childW) / 2; float posX = childW / 2 + r - r * cos(i * 360.f / (count - 1) + dθ); float posY = childH / 2 + r - r * sin(i * 360.f / (count - 1) + dθ); int leftPos = (int) (posX - childW / 2); int topPos = (int) (posY - childH / 2); childView.layout(leftPos, topPos, leftPos + childW, topPos + childH); } } 复制代码
2.ValueAnimator走起
在点击的时候触发mAnimator.start()即可
mAnimator = ValueAnimator.ofInt(0, 360); mAnimator.setDuration(3000); mAnimator.addUpdateListener(a -> { int deg = (int) a.getAnimatedValue(); layoutCircle(1, deg); }); 复制代码
3.位置交换的功能
这里实现和中心的交换,并且加入移动动画
无动画 | 有动画 |
---|---|
---->[维护成员变量]------------- private int centerId = 0;//默认中心点 /** * 交换两个View的位置 * @param positionMe 点击者 * @param positionHe 目标 */ private void swap(int positionMe, int positionHe) { View me = getChildAt(positionMe); View he = getChildAt(positionHe); int TempMeLeft = me.getLeft(); int TempMeTop = me.getTop(); int TempMeRight = me.getRight(); int TempMeBottom = me.getBottom(); me.layout(he.getLeft(), he.getTop(), he.getRight(), he.getBottom()); he.layout(TempMeLeft, TempMeTop, TempMeRight, TempMeBottom); centerId = positionMe; } |--然后只需要在需要的时候触发即可: swap(position, centerId); 复制代码
动画,刚才貌似写过了,直接拿来用
/** * 交换两个View的位置 * @param positionMe 点击者 * @param positionHe 目标 */ private void swapWithAnim(int positionMe, int positionHe) { View me = getChildAt(positionMe); View he = getChildAt(positionHe); int TempMeLeft = me.getLeft(); int TempMeTop = me.getTop(); useLayoutAnimate(me, he.getLeft(), he.getTop()); useLayoutAnimate(he, TempMeLeft,TempMeTop); centerId = positionMe; } private void useLayoutAnimate(View view, int x, int y) { ObjectAnimator.ofInt(view, "Left", x).setDuration(500).start(); ObjectAnimator.ofInt(view, "Top", y).setDuration(500).start(); ObjectAnimator.ofInt(view, "Right", x + view.getMeasuredWidth()).setDuration(500).start(); ObjectAnimator.ofInt(view, "Bottom", y + view.getMeasuredHeight()).setDuration(500).start(); } 复制代码
既然可以动画,那么则么玩都可以,比如旋转和放大
动画就不展开了,详情可见: Android 动画 Animator 家族使用指南
旋转 | 放大 |
---|---|
三、你觉得无聊,玩点6的
1.神技之一: VelocityTracker
这个类估计听过的人不多,翻译出来是 速度追踪器
,作为一个好用的类,在此拎出来讲一讲
它的作用是获取你滑动的x,y的速度 x 左负,y上负
---->[FlowerLayout#init]--------------- private void init(AttributeSet attrs) { ... velocityTracker = VelocityTracker.obtain();//1.VelocityTracker的创建 } ---->[FlowerLayout#onTouchEvent]--------------- @Override public boolean onTouchEvent(MotionEvent event) { View centerView = getChildAt(0); velocityTracker.addMovement(event);//2.VelocityTracker与event结合 switch (event.getAction()) { case MotionEvent.ACTION_DOWN: ... break; case MotionEvent.ACTION_MOVE: velocityTracker.computeCurrentVelocity(1000);//3.计算速度 //4.获取值 Log.e(TAG, "X velocity: " + velocityTracker.getXVelocity()+ "--Y velocity: " + velocityTracker.getYVelocity()); break; case MotionEvent.ACTION_UP: ... break; } return true; } |--注意第5点:在适当的地方取消和回收 velocityTracker.clear();//取消 velocityTracker.recycle();//回收 |---我们比较在意的是计算速度的方法,1000是搞嘛的? /** * Equivalent to invoking {@link #computeCurrentVelocity(int, float)} with a maximum * velocity of Float.MAX_VALUE. * 也就是说这里的第三参是Float的最大值,表示这个速度足以超光速 * @see #computeCurrentVelocity(int, float) */ public void computeCurrentVelocity(int units) { nativeComputeCurrentVelocity(mPtr, units, Float.MAX_VALUE); } /** * @param units The units you would like the velocity in. A value of 1 * provides pixels per millisecond, 1000 provides pixels per second, etc. 你想要的单位是速度。值1表示像素/毫秒,1000表示像素/秒,等等。 * @param maxVelocity The maximum velocity that can be computed by this method. * This value must be declared in the same unit as the units parameter. This value * must be positive. 该方法可以计算的最大值 */ public void computeCurrentVelocity(int units, float maxVelocity) { nativeComputeCurrentVelocity(mPtr, units, maxVelocity); } |-- native方法就不挖了 复制代码
2.有了速度能干嘛?
注
接下来的这部分源于陈小缘的 Android实现圆弧滑动效果之ArcSlidingHelper篇
我认真研究了一下,并融入了本ViewGroup,他封装的非常好,我拆了一下截取了和惯性相关的部分
不懂的可以去深度一下,我就不卖弄唇舌了,GitHub在: ArcSlidingHelper
---->[FlowerLayout#onLayout]-------------------- private void initRotate() { int width = getWidth(); int height = getHeight(); mPivotX = width/2; mPivotY = height/2; mVelocityTracker = VelocityTracker.obtain(); mScrollAvailabilityRatio = .3F; } @Override public boolean onTouchEvent(MotionEvent event) { View centerView = getChildAt(0); float x, y; x = event.getRawX(); y = event.getRawY(); mVelocityTracker.addMovement(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mAnimator.start(); abortAnimation(); centerView.layout(x, y, x + centerView.getMeasuredWidth(), y + centerView.getMeasuredHeight()); Log.e("EVENT", "onTouchEvent: " + x + "------" + y); break; case MotionEvent.ACTION_MOVE: handleActionMove(x, y); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_OUTSIDE: mVelocityTracker.computeCurrentVelocity(1000); mScroller.fling(0, 0, (int) mVelocityTracker.getXVelocity(), (int) mVelocityTracker.getYVelocity(), Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); startFling(); break; } mStartX = x; mStartY = y; return true; } //------------------------惯性旋转---------------------------- private Scroller mScroller = new Scroller(getContext()); private int mPivotX, mPivotY; private float mStartX, mStartY; private float mLastScrollOffset; private float mScrollAvailabilityRatio; private boolean isClockwiseScrolling; private boolean isShouldBeGetY; private boolean isRecycled; private VelocityTracker mVelocityTracker; private Handler mHandler = new Handler(msg -> { computeInertialSliding(); return false; }); /** * 处理惯性滚动 */ private void computeInertialSliding() { checkIsRecycled(); if (mScroller.computeScrollOffset()) { float y = ((isShouldBeGetY ? mScroller.getCurrY() : mScroller.getCurrX()) * mScrollAvailabilityRatio); if (mLastScrollOffset != 0) { float offset = fixAngle(Math.abs(y - mLastScrollOffset)); float deg = isClockwiseScrolling ? offset : -offset; setRotation(getRotation() + deg); } mLastScrollOffset = y; startFling(); } else if (mScroller.isFinished()) { mLastScrollOffset = 0; } } /** * 计算滑动的角度 */ private void handleActionMove(float x, float y) { float l, t, r, b; if (mStartX > x) { r = mStartX; l = x; } else { r = x; l = mStartX; } if (mStartY > y) { b = mStartY; t = y; } else { b = y; t = mStartY; } float pA1 = Math.abs(mStartX - mPivotX); float pA2 = Math.abs(mStartY - mPivotY); float pB1 = Math.abs(x - mPivotX); float pB2 = Math.abs(y - mPivotY); float hypotenuse = (float) Math.sqrt(Math.pow(r - l, 2) + Math.pow(b - t, 2)); float lineA = (float) Math.sqrt(Math.pow(pA1, 2) + Math.pow(pA2, 2)); float lineB = (float) Math.sqrt(Math.pow(pB1, 2) + Math.pow(pB2, 2)); if (hypotenuse > 0 && lineA > 0 && lineB > 0) { float angle = fixAngle((float) Math.toDegrees(Math.acos((Math.pow(lineA, 2) + Math.pow(lineB, 2) - Math.pow(hypotenuse, 2)) / (2 * lineA * lineB)))); float deg = (isClockwiseScrolling = isClockwise(x, y)) ? angle : -angle; setRotation(getRotation() + deg); } } /** * 打断动画 */ public void abortAnimation() { checkIsRecycled(); if (!mScroller.isFinished()) { mScroller.abortAnimation(); } } /** * 释放资源 */ public void release() { checkIsRecycled(); mScroller = null; mVelocityTracker.recycle(); mVelocityTracker = null; isRecycled = true; } /** * 检测手指是否顺时针滑动 * * @param x 当前手指的x坐标 * @param y 当前手指的y坐标 * @return 是否顺时针 */ private boolean isClockwise(float x, float y) { return (isShouldBeGetY = Math.abs(y - mStartY) > Math.abs(x - mStartX)) ? x < mPivotX != y > mStartY : y < mPivotY == x > mStartX; } /** * 开始惯性滚动 */ private void startFling() { mHandler.sendEmptyMessage(0); } /** * 调整角度,使其在360之间 * * @param rotation 当前角度 * @return 调整后的角度 */ private float fixAngle(float rotation) { float angle = 360F; if (rotation < 0) { rotation += angle; } if (rotation > angle) { rotation = rotation % angle; } return rotation; } /** * 检查资源释放已经释放 */ private void checkIsRecycled() { if (isRecycled) { throw new IllegalStateException(" is recycled!"); } } 复制代码
OK,今天就到这里
后记:捷文规范
1.本文成长记录及勘误表
项目源码 | 日期 | 附录 |
---|---|---|
V0.1--github | 2018-2-20 | 无 |
发布名: View篇:玩一下自定义ViewGroup
捷文链接: juejin.im/post/5c6b71…
2.更多关于我
笔名 | 微信 | |
---|---|---|
张风捷特烈 | 1981462002 | zdl1994328 |
我的github: github.com/toly1994328
我的简书: www.jianshu.com/u/e4e52c116…
我的掘金: juejin.im/user/5b42c0…
个人网站:www.toly1994.com
3.声明
1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4----看到这里,我在此感谢你的喜欢与支持
以上所述就是小编给大家介绍的《View篇:玩一下自定义ViewGroup》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Android 自定义 View (04自定义属性)
- Vue自定义组件(简单实现一个自定义组件)
- Android 自定义View:深入理解自定义属性(七)
- Qt编写自定义控件20-自定义饼图 原 荐
- SpringBoot2 | SpringBoot自定义AutoConfiguration | SpringBoot自定义starter(五)
- 『互联网架构』软件架构-springboot自定义视图和自定义Starter(90)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
100个可操作的网络赚钱方法
陶秋丰 / 云南科技 / 2009-12 / 29.80元
《100个可操作的网络赚钱方法》专为有志于网上创业的读者量身打造,作者是“实战型”的网赚高手,在17岁时就通过互联网创业“年人10万”,如今结合自身的亲身实战经验,与大家分享可以实实在在盈利的100个网络赚钱方法和技巧。内容包括:网站创建与推广、竞价广告、联盟赚钱、网站SEO优化、域名投资、广告投放盈利、威客、博客、淘客赚钱等多个方面。 本手册中作者结合自身的网络赚钱经历,通过具体的、可操作......一起来看看 《100个可操作的网络赚钱方法》 这本书的介绍吧!