地球仪式分布的控件,球体控件

效果:
这里写图片描述

本人爱好做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" />

自定义控件往往与项目要的效果有些出入,还是要理解了,然后再修改些内容再使用最好

完整代码稍后上传

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值