效果:
本人爱好做android各种好玩的效果和交互,欢迎大家交流。
引用大神的成果
http://www.open-open.com/lib/view/open1455706317480.html
原来的效果是这样的
虽然我添加了3D翻转,还有光照背景,看起来华丽,但其实不知道在什么地方用得上。又修改了代码,添加了滑动停止后,自动锁定最靠近中央的item,作为一个选择菜单使用,跟日期选择控件有点类似。
给这个控件的每个item添加触发点击事件,应该是不太需要的吧。
毕竟,每个都要点击,然后触发比如查看大图之类,还不如用最直观的gridview呢。
代码:
从大神的工程的基础上重构了实体类Tag.java的一些属性名字,比如用简单的x,y,z表示子控件的3d空间坐标
public class Tag {
private int popularity; //this is the importance/popularity of the Tag
public float x, y, z; //the center of the 3D Tag
private float loc2DX, loc2DY;
private float scale;
private float[] argb;
private static final int DEFAULT_POPULARITY = 5;
修改了TagCloudView.java。
去掉onLayout方法,改沿用继承FrameLayout了,修改updateChild方法,view的位置,大小,透明度,X,Y旋转计算直接完成,然后invalidate()重新绘制。
修改onTouchEvent,不再根据触摸点离中心有多远,而是根据手指滑动距离,决定X,Y方向的角度偏转修改对惯性摩擦阻力的一些参数(run方法),由于修改了角度偏转方法,如果还按原来的参数,则阻力太小,根本停不下来
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import com.dxtx.common.R;
/**
* Copyright © 2016 moxun
* <p/>
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the “Software”),
* to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* <p/>
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* <p/>
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
public class TagCloudView extends FrameLayout implements Runnable, TagsAdapter.OnDataSetChangeListener {
// private final float TOUCH_SCALE_FACTOR = .8f;
private final float TOUCH_SCALE_FACTOR = 12f;
private final float TRACKBALL_SCALE_FACTOR = 10;
private float tspeed = 2f;
private TagCloud mTagCloud;
private float mAngleX = 0.5f;
private float mAngleY = 0.5f;
private float centerX, centerY;
private float radius;
private float radiusPercent = 0.9f;
private float[] darkColor = new float[]{1f, 0f, 0f, 1};
private float[] lightColor = new float[]{0.9412f, 0.7686f, 0.2f, 1};
private int sphereColor = Color.WHITE;
private State touch_state = State.Scroll;
private Tag selectTag;
private int selectIndex;
private float minLockError;
private enum State {
Touch, Calm, AutoLock, Scroll
}
private boolean autoSelect = false;
private Handler handler = new Handler(Looper.getMainLooper());
private TagsAdapter tagsAdapter;
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Paint glancePaint;
private RectF glanceRect = new RectF();
private RectF shadowRect = new RectF();
private Bitmap glanceBitmap, shadowBitmap;
public TagCloudView(Context context) {
super(context);
setFocusableInTouchMode(true);
init(context, null);
}
public TagCloudView(Context context, AttributeSet attrs) {
super(context, attrs);
setFocusableInTouchMode(true);
init(context, attrs);
}
public TagCloudView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setFocusableInTouchMode(true);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
mTagCloud = new TagCloud();
if (attrs != null) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TagCloudView);
int light = typedArray.getColor(R.styleable.TagCloudView_lightColor, Color.GREEN);
setLightColor(light);
int dark = typedArray.getColor(R.styleable.TagCloudView_darkColor, Color.GRAY);
setDarkColor(dark);
sphereColor = typedArray.getColor(R.styleable.TagCloudView_sphereColor, Color.WHITE);
autoSelect = typedArray.getBoolean(R.styleable.TagCloudView_autoLockSelect, autoSelect);
float p = typedArray.getFloat(R.styleable.TagCloudView_radiusPercent, radiusPercent);
setRadiusPercent(p);
float s = typedArray.getFloat(R.styleable.TagCloudView_scrollSpeed, 2f);
setScrollSpeed(s);
}
paint.setColor(sphereColor);
glancePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
glancePaint.setDither(true);
glancePaint.setFilterBitmap(true);
glancePaint.setAlpha(200);
glanceBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ball_light);
shadowBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.shadow);
minLockError = getResources().getDimension(R.dimen._1dp);
}
public final void setAdapter(TagsAdapter adapter) {
tagsAdapter = adapter;
tagsAdapter.setOnDataSetChangeListener(this);
onChange();
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
getViewTreeObserver().removeGlobalOnLayoutListener(this);
//设置每个item的旋转中心
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.setPivotX(child.getMeasuredWidth() / 2);
child.setPivotY(child.getMeasuredHeight() / 2);
}
updateChild();
}
});
}
public void setLightColor(int color) {
float[] argb = new float[4];
argb[0] = Color.alpha(color) / 1.0f / 255;
argb[1] = Color.red(color) / 1.0f / 255;
argb[2] = Color.green(color) / 1.0f / 255;
argb[3] = Color.blue(color) / 1.0f / 255;
lightColor = argb.clone();
onChange();
}
public void setDarkColor(int color) {
float[] argb = new float[4];
argb[0] = Color.alpha(color) / 1.0f / 255;
argb[1] = Color.red(color) / 1.0f / 255;
argb[2] = Color.green(color) / 1.0f / 255;
argb[3] = Color.blue(color) / 1.0f / 255;
darkColor = argb.clone();
onChange();
}
public void setRadiusPercent(float percent) {
if (percent > 1f || percent < 0f) {
throw new IllegalArgumentException("percent value not in range 0 to 1");
} else {
radiusPercent = percent;
onChange();
}
}
private void initFromAdapter() {
this.postDelayed(new Runnable() {
@Override
public void run() {
mTagCloud.setRadius((int) radius);
mTagCloud.setTagColorLight(lightColor);//higher color
mTagCloud.setTagColorDark(darkColor);//lower color
mTagCloud.clear();
removeAllViews();
for (int i = 0; i < tagsAdapter.getCount(); i++) {
TagCloudView.this.mTagCloud.add(new Tag(tagsAdapter.getPopularity(i)));
addView(tagsAdapter.getView(getContext(), i, TagCloudView.this));
}
mTagCloud.create(true);
mTagCloud.setAngleX(mAngleX);
mTagCloud.setAngleY(mAngleY);
mTagCloud.update();
}
}, 0);
}
public void setScrollSpeed(float scrollSpeed) {
tspeed = scrollSpeed;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int modeW = MeasureSpec.getMode(widthMeasureSpec);
int modeH = MeasureSpec.getMode(heightMeasureSpec);
if (modeW == MeasureSpec.EXACTLY) {
heightMeasureSpec = widthMeasureSpec;
} else if (modeH == MeasureSpec.EXACTLY) {
widthMeasureSpec = heightMeasureSpec;
}
//宽高都wrap_content,指定最大值,则用最大值
else if (modeW == MeasureSpec.AT_MOST && modeH == MeasureSpec.AT_MOST) {
int size = Math.min(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(widthMeasureSpec));
widthMeasureSpec = heightMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
} else if (modeW == MeasureSpec.AT_MOST) {
int size = MeasureSpec.getSize(widthMeasureSpec);
widthMeasureSpec = heightMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
} else if (modeH == MeasureSpec.AT_MOST) {
int size = MeasureSpec.getSize(heightMeasureSpec);
widthMeasureSpec = heightMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
}
//如果父控件让我宽高都随意,则我也随意
else {
widthMeasureSpec = heightMeasureSpec = MeasureSpec.makeMeasureSpec(600, MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
handler.post(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
handler.removeCallbacksAndMessages(null);
}
private void updateChild() {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
Tag tag = mTagCloud.get(i);
float scale = tag.getScale();
child.setScaleX(scale);
child.setScaleY(scale);
if (scale < 1 && scale > 0.9f) {//[0.9,1]短暂区间的强制转为区间[0.1,1]
scale = 9 * scale - 8;
} else if (scale <= 0.9f) {
scale = 0.1f;//让转到背面的看起来模糊,
}
child.setAlpha(scale);
int left, top;
left = (int) (centerX + tag.x - child.getMeasuredWidth() / 2f);
top = (int) (centerY + tag.y - child.getMeasuredHeight() / 2f);
child.setX(left);
child.setY(top);
double rx = Math.sqrt(radius * radius - tag.x * tag.x);
float rotationX = -(float) (Math.asin(tag.y / rx) * 180 / Math.PI);
float rotationY = -(float) (Math.asin(-tag.x / radius) * 180 / Math.PI);
child.setRotationX(rotationX);
child.setRotationY(rotationY);
}
}
invalidate();
}
public void reset() {
mTagCloud.reset();
updateChild();
}
@Override
public boolean onTrackballEvent(MotionEvent e) {
float x = e.getX();
float y = e.getY();
mAngleX = (y) * tspeed * TRACKBALL_SCALE_FACTOR;
mAngleY = (-x) * tspeed * TRACKBALL_SCALE_FACTOR;
mTagCloud.setAngleX(mAngleX);
mTagCloud.setAngleY(mAngleY);
mTagCloud.update();
updateChild();
return true;
}
float x0, y0, downX, downY;
@Override
public boolean dispatchTouchEvent(MotionEvent e) {
super.dispatchTouchEvent(e);
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
touch_state = State.Touch;
x0 = x;
y0 = y;
break;
case MotionEvent.ACTION_MOVE:
//rotate elements depending on how far the selection point is from center of cloud
// float dx = x - centerX;
// float dy = y - centerY;
if (selectTag != null) {
selectTag = null;
getChildAt(selectIndex).setSelected(false);
}
float dx = x - x0;
float dy = y - y0;
x0 = x;
y0 = y;
mAngleX = (dy / radius) * tspeed * TOUCH_SCALE_FACTOR;
mAngleY = (-dx / radius) * tspeed * TOUCH_SCALE_FACTOR;
processTouch();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//开始惯性滚动
touch_state = State.Scroll;
break;
}
return true;
}
private boolean intercept = false;
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
// touch_state = State.Touch;
downX = x;
downY = y;
break;
case MotionEvent.ACTION_MOVE:
if (intercept) {
return true;
}
float dx = x - downX;
float dy = y - downY;
if (Math.sqrt(dx * dx + dy * dy) > 100) {//滑动超过一定距离,拦截事件
return intercept = true;
}
break;
case MotionEvent.ACTION_UP:
boolean temp = intercept;
intercept = false;
return temp;
}
return false;
}
private void processTouch() {
if (mTagCloud != null) {
mTagCloud.setAngleX(mAngleX);
mTagCloud.setAngleY(mAngleY);
mTagCloud.update();
}
updateChild();
}
@Override
public void onChange() {
if (!isInEditMode())
initFromAdapter();
}
@Override
public void run() {
if (touch_state == State.Scroll) {
//是否已经停止滚动
boolean stop = mAngleX == 0 && mAngleY == 0;
if (mAngleX > 0.4f) {
mAngleX -= 0.1f;
} else if (mAngleX < -0.4f) {
mAngleX += 0.1f;
} else {
mAngleX = 0;
}
if (mAngleY > 0.4f) {
mAngleY -= 0.1f;
} else if (mAngleY < -0.4f) {
mAngleY += 0.1f;
} else {
mAngleY = 0;
}
if (autoSelect && !stop && mAngleX == 0 && mAngleY == 0) {
startAutoLockItem();
}
processTouch();
} else if (touch_state == State.AutoLock) {
float absX = Math.abs(selectTag.x);
float absY = Math.abs(selectTag.y);
//是否在误差范围内
boolean locked = selectTag == null || (absX < 2 && absY < 2);
if (locked) {
//锁定完成,恢复平静---
mAngleX = mAngleY = 0;
touch_state = State.Calm;
handler.postDelayed(this, 16);
View child = getChildAt(selectIndex);
child.setSelected(true);
if (onItemSelectListener != null) {
onItemSelectListener.onSelectLockedComplete(child, selectIndex, selectTag);
}
return;
}
if (absX > absY) {
if (absX < minLockError) {
mAngleX = mAngleY = 0;
} else {
mAngleY = Math.max(absX / radius * 3.5f, .5f);
mAngleX = absY / absX * mAngleY;
mAngleY *= selectTag.x / absX;
mAngleX *= -selectTag.y / absY;
}
} else {
if (absY < minLockError) {
mAngleY = mAngleX = 0;
} else {
mAngleX = Math.max(absY / radius * 3.5f, .5f);
mAngleY = absX / absY * mAngleX;
mAngleX *= -selectTag.y / absY;
mAngleY *= selectTag.x / absX;
}
}
processTouch();
}
handler.postDelayed(this, 16);
}
@Override
protected void dispatchDraw(Canvas canvas) {
//绘制影子
canvas.drawBitmap(shadowBitmap, null, shadowRect, glancePaint);
//绘制地球仪
canvas.drawCircle(centerX, centerY, radius, paint);
//绘制光照
canvas.drawBitmap(glanceBitmap, null, glanceRect, glancePaint);
super.dispatchDraw(canvas);
//画线
/* List<Tag> list = mTagCloud.getTagList();
if (list != null && !list.isEmpty()) {
Path path = new Path();
Tag lastTag = mTagCloud.get(0);
path.moveTo(centerX + lastTag.getX(), centerY + lastTag.getY());
for (int i = 1; i < getChildCount(); i++) {
Tag tag = mTagCloud.get(i);
path.lineTo(centerX + tag.x, centerY + tag.y);
}
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(4);
canvas.drawPath(path, paint);
}*/
}
@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 = radiusPercent * Math.min(centerX, centerY);
glanceRect.set(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
shadowRect.set(centerX, centerY + radius * 0.8f, w, centerY + radius * 1.05f);
}
//开始自动锁定item
private void startAutoLockItem() {
if (selectTag != null) {
getChildAt(selectIndex).setSelected(false);
selectTag = null;
}
//选出距离最近的item
float max = Integer.MAX_VALUE;
for (int i = 0; i < getChildCount(); i++) {
Tag tag = mTagCloud.get(i);
if (tag.getScale() > 0.9) {
double distance = tag.distance();
if (distance < max) {
max = (float) distance;
selectTag = tag;
selectIndex = i;
}
}
}
if (selectTag != null) {
touch_state = State.AutoLock;
if (onItemSelectListener != null) {
onItemSelectListener.onSelect(getChildAt(selectIndex), selectIndex, selectTag);
}
}
}
public void setOnItemSelectListener(OnItemSelectListener onItemSelectListener) {
this.onItemSelectListener = onItemSelectListener;
}
private OnItemSelectListener onItemSelectListener;
public interface OnItemSelectListener {
void onSelect(View child, int position, Tag tag);
void onSelectLockedComplete(View child, int position, Tag tag);
}
}
自定义属性
<declare-styleable name="TagCloudView">
<attr name="lightColor" format="color" />
<attr name="darkColor" format="color" />
<attr name="radiusPercent" format="float" />
<attr name="scrollSpeed" format="float" />
<attr name="sphereColor" format="color" />
<attr name="autoLockSelect" format="boolean" />
</declare-styleable>
缺少一些Resource的话,参照:球体
阴影:
R.dimen._1dp ,这个就是<dimen name="_1dp">1dp</dimen>
调用示例
package com.example.user.third;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.example.user.mytest.R;
/**
* Created by user on 2016/12/7.
*/
public class ThirdActivity extends Activity {
TagCloudView cloudView;
private int[] ss = {R.mipmap.baby1, R.mipmap.jt, R.mipmap.liy, R.mipmap.lyf1, R.mipmap.ym, R.mipmap.mm1, R.mipmap.mm2, R.mipmap.baby1, R.mipmap.jt, R.mipmap.liy, R.mipmap.lyf1, R.mipmap.ym, R.mipmap.mm1, R.mipmap.mm2};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_third);
cloudView = (TagCloudView) findViewById(R.id.tagCloudView1);
cloudView.setAdapter(new TagsAdapter() {
@Override
public int getCount() {
return ss.length;
}
@Override
public View getView(Context context, int position, ViewGroup parent) {
ImageView imageView = new ImageView(context);
imageView.setLayoutParams(new ViewGroup.LayoutParams(160, 160));
imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
imageView.setImageResource(ss[position]);
return imageView;
}
@Override
public Object getItem(int position) {
return ss[position];
}
@Override
public int getPopularity(int position) {
return ss.length - position;
}
});
cloudView.setOnItemSelectListener(new TagCloudView.OnItemSelectListener() {
@Override
public void onSelect(View child, int position, Tag tag) {
//准备锁定child时调用
}
@Override
public void onSelectLockedComplete(View child, int position, Tag tag) {
//当锁定item的动画完成后调用
}
});
}
}
<com.dxtx.widget.tagcloud.TagCloudView
android:id="@+id/tagCloudView1"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:sphereColor="#0ff"
app:radiusPercent="0.9"
app:scrollSpeed="3"
app:autoLockSelect="true"
tools:visibility="visible" />
自定义控件往往与项目要的效果有些出入,还是要理解了,然后再修改些内容再使用最好
完整代码稍后上传