本文已授權(quán)微信公眾號:鴻洋(hongyangAndroid)原創(chuàng)首發(fā)捧韵。
之前玩手機QQ時發(fā)現(xiàn)下面的圖標(biāo)竟然可以拖拽市咆,發(fā)現(xiàn)還蠻好玩的。于是自己也模仿著寫了一個再来。
先上個效果圖吧
實現(xiàn)的方式有很多蒙兰,我說一下我的思路:我的思路比較簡單,無非就是上下兩層圖片可拖動的范圍和速度不一樣唄(大圖標(biāo)拖動范圍和速度小于小圖標(biāo)拖動范圍和速度)芒篷。
備注(以第一個消息圖標(biāo)為例):大圖標(biāo)指的是外面的氣泡圖標(biāo)搜变,小圖標(biāo)指的是氣泡里面的眼睛和嘴巴圖標(biāo)。切圖時將一張整體圖片切成了這兩個圖標(biāo)针炉。具體可下載Demo參考里面的圖片資源挠他。
自定義屬性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="QQNaviView">
<attr name="bigIconSrc" format="reference"/>
<attr name="smallIconSrc" format="reference"/>
<attr name="iconWidth" format="dimension"/>
<attr name="iconHeight" format="dimension"/>
<attr name="range" format="float"/>
</declare-styleable>
</resources>
其中range為可拖動的范圍(其實是倍數(shù)),默認(rèn)值是1篡帕,不宜設(shè)置過大殖侵。
主要的拖動代碼
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
float deltaX = x - lastX;
float deltaY = y - lastY;
moveEvent(mBigIcon, deltaX, deltaY, mSmallRadius);
//因為可拖動大半徑是小半徑的1.5倍贸呢, 因此這里x,y也相應(yīng)乘1.5
moveEvent(mSmallIcon, 1.5f * deltaX, 1.5f * deltaY, mBigRadius);
break;
case MotionEvent.ACTION_UP:
//抬起時復(fù)位
mBigIcon.setX(0);
mBigIcon.setY(0);
mSmallIcon.setX(0);
mSmallIcon.setY(0);
break;
}
return super.onTouchEvent(event);
}
這里先得到X軸拖動的距離deltaX和Y軸拖動的距離deltaY,大圖標(biāo)對應(yīng)小半徑拢军,小圖標(biāo)對應(yīng)大半徑楞陷。然后看moveEvent方法:
private void moveEvent(View view, float deltaX, float deltaY, float radius){
//先計算拖動距離
float distance = getDistance(deltaX, deltaY);
//拖動的方位角,atan2出來的角度是帶正負(fù)號的
double degree = Math.atan2(deltaY, deltaX);
//如果大于臨界半徑就不能再往外拖了
if (distance > radius){
view.setX(view.getLeft() + (float) (radius * Math.cos(degree)));
view.setY(view.getTop() + (float) (radius * Math.sin(degree)));
}else {
view.setX(view.getLeft() + deltaX);
view.setY(view.getTop() + deltaY);
}
}
方法很簡單茉唉,注釋結(jié)合這張圖就一目了然了固蛾,主要是注意在抬起時圖標(biāo)復(fù)位就好了。
簡單看一下初始化
由于圖標(biāo)下面一般會帶文字度陆,因此直接繼承了LinearLayout魏铅,并且默認(rèn)設(shè)置成了垂直排列。
public QQNaviView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.QQNaviView, defStyleAttr, 0);
mBigIconSrc = ta.getResourceId(R.styleable.QQNaviView_bigIconSrc, R.drawable.big);
mSmallIconSrc = ta.getResourceId(R.styleable.QQNaviView_smallIconSrc, R.drawable.small);
mIconWidth = ta.getDimension(R.styleable.QQNaviView_iconWidth, dp2px(context, 60));
mIconHeight = ta.getDimension(R.styleable.QQNaviView_iconHeight, dp2px(context, 60));
mRange = ta.getFloat(R.styleable.QQNaviView_range, 1);
ta.recycle();
//默認(rèn)垂直排列
setOrientation(LinearLayout.VERTICAL);
init(context);
}
在init方法中進(jìn)行了布局文件的綁定坚芜,并且讓該view水平居中览芳。
private void init(Context context) {
mView = inflate(context, R.layout.view_icon, null);
mBigIcon = (ImageView) mView.findViewById(R.id.iv_big);
mSmallIcon = (ImageView) mView.findViewById(R.id.iv_small);
mBigIcon.setImageResource(mBigIconSrc);
mSmallIcon.setImageResource(mSmallIconSrc);
setWidthAndHeight(mBigIcon);
setWidthAndHeight(mSmallIcon);
LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
lp.gravity = Gravity.CENTER_HORIZONTAL;
mView.setLayoutParams(lp);
addView(mView);
}
這里值得注意的是onMeasure方法。由于圖標(biāo)可以往外拖動鸿竖,所以要給ImageView一個默認(rèn)的padding沧竟,不然拖動時最外面部分會消失。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setupView();
final int w = resolveSize(getMeasuredWidth(), widthMeasureSpec);
final int h = resolveSize(getMeasuredHeight(), heightMeasureSpec);
setMeasuredDimension(w, h);
}
/**
* 確定view以及拖動相關(guān)參數(shù)
*/
private void setupView() {
//根據(jù)view的寬高確定可拖動半徑的大小缚忧,這里要用getMeasuredWidth和getMeasuredHeight
mSmallRadius = 0.1f * Math.min(mView.getMeasuredWidth(), mView.getMeasuredHeight()) * mRange;
mBigRadius = 1.5f * mSmallRadius;
//設(shè)置imageview的padding悟泵,不然拖動時圖片邊緣部分會消失
int padding = (int) mBigRadius;
mBigIcon.setPadding(padding, padding, padding, padding);
mSmallIcon.setPadding(padding, padding, padding, padding);
}
然后就沒啥好說了,直接看源碼吧闪水。
源碼:
public class QQNaviView extends LinearLayout {
private static final String TAG = "QQNaviView";
private Context mContext;
/* 主view */
private View mView;
/* 外層icon/拖動幅度較小icon */
private ImageView mBigIcon;
/* 里層icon/拖動幅度較大icon */
private ImageView mSmallIcon;
/* 外層icon資源 */
private int mBigIconSrc;
/* 里面icon資源 */
private int mSmallIconSrc;
/* icon寬度 */
private float mIconWidth;
/* icon高度 */
private float mIconHeight;
/* 拖動幅度較大半徑 */
private float mBigRadius;
/* 拖動幅度小半徑 */
private float mSmallRadius;
/* 拖動范圍 可調(diào) */
private float mRange;
private float lastX;
private float lastY;
public QQNaviView(@NonNull Context context) {
this(context, null);
}
public QQNaviView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public QQNaviView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.QQNaviView, defStyleAttr, 0);
mBigIconSrc = ta.getResourceId(R.styleable.QQNaviView_bigIconSrc, R.drawable.big);
mSmallIconSrc = ta.getResourceId(R.styleable.QQNaviView_smallIconSrc, R.drawable.small);
mIconWidth = ta.getDimension(R.styleable.QQNaviView_iconWidth, dp2px(context, 60));
mIconHeight = ta.getDimension(R.styleable.QQNaviView_iconHeight, dp2px(context, 60));
mRange = ta.getFloat(R.styleable.QQNaviView_range, 1);
ta.recycle();
//默認(rèn)垂直排列
setOrientation(LinearLayout.VERTICAL);
init(context);
}
private void init(Context context) {
mView = inflate(context, R.layout.view_icon, null);
mBigIcon = (ImageView) mView.findViewById(R.id.iv_big);
mSmallIcon = (ImageView) mView.findViewById(R.id.iv_small);
mBigIcon.setImageResource(mBigIconSrc);
mSmallIcon.setImageResource(mSmallIconSrc);
setWidthAndHeight(mBigIcon);
setWidthAndHeight(mSmallIcon);
LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
lp.gravity = Gravity.CENTER_HORIZONTAL;
mView.setLayoutParams(lp);
addView(mView);
}
/**
* 設(shè)置icon寬高
* @param view
*/
private void setWidthAndHeight(View view){
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) view.getLayoutParams();
lp.width = (int) mIconWidth;
lp.height = (int) mIconHeight;
view.setLayoutParams(lp);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setupView();
final int w = resolveSize(getMeasuredWidth(), widthMeasureSpec);
final int h = resolveSize(getMeasuredHeight(), heightMeasureSpec);
setMeasuredDimension(w, h);
}
/**
* 確定view以及拖動相關(guān)參數(shù)
*/
private void setupView() {
//根據(jù)view的寬高確定可拖動半徑的大小
mSmallRadius = 0.1f * Math.min(mView.getMeasuredWidth(), mView.getMeasuredHeight()) * mRange;
mBigRadius = 1.5f * mSmallRadius;
//設(shè)置imageview的padding糕非,不然拖動時圖片邊緣部分會消失
int padding = (int) mBigRadius;
mBigIcon.setPadding(padding, padding, padding, padding);
mSmallIcon.setPadding(padding, padding, padding, padding);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int childLeft;
int childTop = 0;
for (int i = 0; i < getChildCount(); i ++){
final View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (child.getVisibility() != GONE){
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
//水平居中顯示
childLeft = (getWidth() - childWidth) / 2;
//當(dāng)前子view的top
childTop += lp.topMargin;
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
//下一個view的top是當(dāng)前子view的top + height + bottomMargin
childTop += childHeight + lp.bottomMargin;
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
float deltaX = x - lastX;
float deltaY = y - lastY;
moveEvent(mBigIcon, deltaX, deltaY, mSmallRadius);
//因為可拖動大半徑是小半徑的1.5倍, 因此這里x,y也相應(yīng)乘1.5
moveEvent(mSmallIcon, 1.5f * deltaX, 1.5f * deltaY, mBigRadius);
break;
case MotionEvent.ACTION_UP:
//抬起時復(fù)位
mBigIcon.setX(0);
mBigIcon.setY(0);
mSmallIcon.setX(0);
mSmallIcon.setY(0);
break;
}
return super.onTouchEvent(event);
}
/**
* 拖動事件
* @param view
* @param deltaX
* @param deltaY
* @param radius
*/
private void moveEvent(View view, float deltaX, float deltaY, float radius){
//先計算拖動距離
float distance = getDistance(deltaX, deltaY);
//拖動的方位角球榆,atan2出來的角度是帶正負(fù)號的
double degree = Math.atan2(deltaY, deltaX);
//如果大于臨界半徑就不能再往外拖了
if (distance > radius){
view.setX(view.getLeft() + (float) (radius * Math.cos(degree)));
view.setY(view.getTop() + (float) (radius * Math.sin(degree)));
}else {
view.setX(view.getLeft() + deltaX);
view.setY(view.getTop() + deltaY);
}
}
private int dp2px(Context context, float dpVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dpVal, context.getResources().getDisplayMetrics());
}
private float getDistance(float x, float y){
return (float) Math.sqrt(x * x + y * y);
}
public void setBigIcon(int res){
mBigIcon.setImageResource(res);
}
public void setSmallIcon(int res){
mSmallIcon.setImageResource(res);
}
public void setIconWidthAndHeight(float width, float height){
mIconWidth = dp2px(mContext, width);
mIconHeight = dp2px(mContext, height);
setWidthAndHeight(mBigIcon);
setWidthAndHeight(mSmallIcon);
}
public void setRange(float range){
mRange = range;
}
}
name | format | description |
---|---|---|
bigIconSrc | reference | 大圖標(biāo)資源 |
smallIconSrc | reference | 小圖標(biāo)資源 |
iconWidth | dimension | 圖標(biāo)寬度 |
iconHeight | dimension | 圖標(biāo)高度 |
range | float | 可拖動范圍 |
PS:一些手機上沒有效果應(yīng)該是setupView()方法中之前用的是mView.getWidth()和mView.getMeasuredHeight()朽肥,應(yīng)該改為mView.getMeasuredWidth和mView.getMeasuredHeight(),感謝gitkanglei的指出持钉。關(guān)于兩者的區(qū)別可參考:
http://blog.csdn.net/dmk877/article/details/49734869/
name | format | description |
---|---|---|
bigIconSrc | reference | 大圖標(biāo)資源 |
smallIconSrc | reference | 小圖標(biāo)資源 |
iconWidth | dimension | 圖標(biāo)寬度 |
iconHeight | dimension | 圖標(biāo)高度 |
range | float | 可拖動范圍 |
如果有其他的實現(xiàn)方式衡招,或者代碼中有什么不合理的地方,歡迎交流~
源碼地址:https://github.com/XingdongYu/QQNaviView歡迎star每强,rua~