您好,登錄后才能下訂單哦!
Android中怎么自定義控件,針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
首先我們從最基本的原理開始分析,看一張圖:
這個圖該怎么繪制呢?實際上我們這里是先繪制兩個圓,然后將兩個圓的切點通過貝塞爾曲線連接起來就達到這個效果了。至于貝塞爾曲線的概念,這里就不多做解釋了,百度一下就知道了。
切點怎么算呢,這里我們稍微復習一些初中的數(shù)學知識??戳诉@個圖之后,求出四個切點應該是輕而易舉了。
現(xiàn)在思路已經(jīng)很清晰了,按照我們的思路,開擼。
首先是我們計算切點以及各坐標點的工具類
public class GeometryUtils { /** * As meaning of method name. * 獲得兩點之間的距離 * @param p0 * @param p1 * @return */ public static float getDistanceBetween2Points(PointF p0, PointF p1) { float distance = (float) Math.sqrt(Math.pow(p0.y - p1.y, 2) + Math.pow(p0.x - p1.x, 2)); return distance; } /** * Get middle point between p1 and p2. * 獲得兩點連線的中點 * @param p1 * @param p2 * @return */ public static PointF getMiddlePoint(PointF p1, PointF p2) { return new PointF((p1.x + p2.x) / 2.0f, (p1.y + p2.y) / 2.0f); } /** * Get point between p1 and p2 by percent. * 根據(jù)百分比獲取兩點之間的某個點坐標 * @param p1 * @param p2 * @param percent * @return */ public static PointF getPointByPercent(PointF p1, PointF p2, float percent) { return new PointF(evaluateValue(percent, p1.x , p2.x), evaluateValue(percent, p1.y , p2.y)); } /** * 根據(jù)分度值,計算從start到end中,fraction位置的值。fraction范圍為0 -> 1 * @param fraction * @param start * @param end * @return */ public static float evaluateValue(float fraction, Number start, Number end){ return start.floatValue() + (end.floatValue() - start.floatValue()) * fraction; } /** * Get the point of intersection between circle and line. * 獲取 通過指定圓心,斜率為lineK的直線與圓的交點。 * * @param pMiddle The circle center point. * @param radius The circle radius. * @param lineK The slope of line which cross the pMiddle. * @return */ public static PointF[] getIntersectionPoints(PointF pMiddle, float radius, Double lineK) { PointF[] points = new PointF[2]; float radian, xOffset = 0, yOffset = 0; if(lineK != null){ radian= (float) Math.atan(lineK); xOffset = (float) (Math.sin(radian) * radius); yOffset = (float) (Math.cos(radian) * radius); }else { xOffset = radius; yOffset = 0; } points[0] = new PointF(pMiddle.x + xOffset, pMiddle.y - yOffset); points[1] = new PointF(pMiddle.x - xOffset, pMiddle.y + yOffset); return points; } }
然后下面看下我們的核心繪制代碼,代碼注釋比較全,此處就不多做解釋了。
/** * 繪制貝塞爾曲線部分以及固定圓 * * @param canvas */ private void drawGooPath(Canvas canvas) { Path path = new Path(); //1. 根據(jù)當前兩圓圓心的距離計算出固定圓的半徑 float distance = (float) GeometryUtils.getDistanceBetween2Points(mDragCenter, mStickCenter); stickCircleTempRadius = getCurrentRadius(distance); //2. 計算出經(jīng)過兩圓圓心連線的垂線的dragLineK(對邊比臨邊)。求出四個交點坐標 float xDiff = mStickCenter.x - mDragCenter.x; Double dragLineK = null; if (xDiff != 0) { dragLineK = (double) ((mStickCenter.y - mDragCenter.y) / xDiff); } //分別獲得經(jīng)過兩圓圓心連線的垂線與圓的交點(兩條垂線平行,所以dragLineK相等)。 PointF[] dragPoints = GeometryUtils.getIntersectionPoints(mDragCenter, dragCircleRadius, dragLineK); PointF[] stickPoints = GeometryUtils.getIntersectionPoints(mStickCenter, stickCircleTempRadius, dragLineK); //3. 以兩圓連線的0.618處作為 貝塞爾曲線 的控制點。(選一個中間點附近的控制點) PointF pointByPercent = GeometryUtils.getPointByPercent(mDragCenter, mStickCenter, 0.618f); // 繪制兩圓連接閉合 path.moveTo((float) stickPoints[0].x, (float) stickPoints[0].y); path.quadTo((float) pointByPercent.x, (float) pointByPercent.y, (float) dragPoints[0].x, (float) dragPoints[0].y); path.lineTo((float) dragPoints[1].x, (float) dragPoints[1].y); path.quadTo((float) pointByPercent.x, (float) pointByPercent.y, (float) stickPoints[1].x, (float) stickPoints[1].y); canvas.drawPath(path, mPaintRed); // 畫固定圓 canvas.drawCircle(mStickCenter.x, mStickCenter.y, stickCircleTempRadius, mPaintRed); }
此時我們已經(jīng)實現(xiàn)了繪制的核心代碼,然后我們加上touch事件的監(jiān)聽,達到動態(tài)的更新dragPoint的中心點位置以及stickPoint半徑的效果。當手抬起的時候,添加一個屬性動畫,達到回彈的效果。
@Override public boolean onTouchEvent(MotionEvent event) { switch (MotionEventCompat.getActionMasked(event)) { case MotionEvent.ACTION_DOWN: { isOutOfRange = false; updateDragPointCenter(event.getRawX(), event.getRawY()); break; } case MotionEvent.ACTION_MOVE: { //如果兩圓間距大于***距離mMaxDistance,執(zhí)行拖拽結(jié)束動畫 PointF p0 = new PointF(mDragCenter.x, mDragCenter.y); PointF p1 = new PointF(mStickCenter.x, mStickCenter.y); if (GeometryUtils.getDistanceBetween2Points(p0, p1) > mMaxDistance) { isOutOfRange = true; updateDragPointCenter(event.getRawX(), event.getRawY()); return false; } updateDragPointCenter(event.getRawX(), event.getRawY()); break; } case MotionEvent.ACTION_UP: { handleActionUp(); break; } default: { isOutOfRange = false; break; } } return true; } /** * 手勢抬起動作 */ private void handleActionUp() { if (isOutOfRange) { // 當拖動dragPoint范圍已經(jīng)超出mMaxDistance,然后又將dragPoint拖回mResetDistance范圍內(nèi)時 if (GeometryUtils.getDistanceBetween2Points(mDragCenter, mStickCenter) < mResetDistance) { //reset return; } // dispappear } else { //手指抬起時,彈回動畫 mAnim = ValueAnimator.ofFloat(1.0f); mAnim.setInterpolator(new OvershootInterpolator(5.0f)); final PointF startPoint = new PointF(mDragCenter.x, mDragCenter.y); final PointF endPoint = new PointF(mStickCenter.x, mStickCenter.y); mAnim.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float fraction = animation.getAnimatedFraction(); PointF pointByPercent = GeometryUtils.getPointByPercent(startPoint, endPoint, fraction); updateDragPointCenter((float) pointByPercent.x, (float) pointByPercent.y); } }); mAnim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { //reset } }); if (GeometryUtils.getDistanceBetween2Points(startPoint, endPoint) < 10) { mAnim.setDuration(100); } else { mAnim.setDuration(300); } mAnim.start(); } }
此時我們拖拽的核心代碼基本都已經(jīng)完成,實際效果如下:
現(xiàn)在小紅點的繪制基本告一段落,我們不得不去思考真正的難點。那就是如何將我們前面的這個GooView應用到實際呢?看實際效果我們的小紅點是放在listView里面的,如果是這樣的話,就代表我們的GooView的拖拽范圍是肯定無法超過父控件item的區(qū)域的。
那么我們要如何實現(xiàn)小紅點可以隨便的在整個屏幕拖拽呢?我們這里稍微整理一下思路。
1.先在listView的item布局中先放入一個小紅點。
2.當我們touch到這個小紅點的時候,隱藏這個小紅點,然后根據(jù)我們布局中小紅點的位置初始化一個GooView并且添加到WindowManager中嗎,達到GooView可以全屏拖動的效果。
3.在添加GooView到WindowManager中的時候,記錄初始小紅點stickPoint的位置,然后根據(jù)stickPoint和dragPointde位置是否超出我們的消失界限來判斷接下來的邏輯。
4.根據(jù)GooView的最終狀態(tài),顯示回彈或者消失動畫。
思路有了,那么就上代碼,根據(jù)***步,我們完成listView的item布局。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="80dp" android:minHeight="80dp"> <ImageView android:id="@+id/iv_head" android:layout_width="50dp" android:layout_height="50dp" android:layout_centerVertical="true" android:layout_marginLeft="20dp" android:src="@mipmap/head"/> <TextView android:id="@+id/tv_content" android:layout_width="wrap_content" android:layout_height="50dp" android:layout_centerVertical="true" android:gravity="center" android:layout_marginLeft="20dp" android:layout_toRightOf="@+id/iv_head" android:text="content - " android:textSize="25sp"/> <LinearLayout android:id="@+id/ll_point" android:layout_width="80dp" android:layout_height="80dp" android:layout_alignParentEnd="true" android:layout_alignParentRight="true" android:layout_alignParentTop="true" android:gravity="center"> <TextView android:id="@+id/point" android:layout_width="wrap_content" android:layout_height="18dp" android:background="@drawable/red_bg" android:gravity="center" android:singleLine="true" android:textColor="@android:color/white" android:textSize="12sp"/> </LinearLayout> </RelativeLayout>
效果如下,要注意的是,對比QQ的真實體驗,小紅點周邊范圍點擊的時候,都是可以直接拖拽小紅點的??紤]到紅點的點擊范圍比較小,所以給紅點增加了一個寬高80dp的父layout,然后我們將touch小紅點事件更改為touch小紅點父layout,這樣只要我們點擊了小紅點的父layout范圍,都會添加GooView到WindowManager中。
接下來第二步,我們完成添加GooView到WindowManager中的代碼。
由于我們的GooView初始添加是從listViewItem中紅點的touch事件開始的,所以我們先完成listView adapter的實現(xiàn)。
public class GooViewAapter extends BaseAdapter { private Context mContext; //記錄已經(jīng)remove的position private HashSet<Integer> mRemoved = new HashSet<Integer>(); private List<String> list = new ArrayList<String>(); public GooViewAapter(Context mContext, List<String> list) { super(); this.mContext = mContext; this.list = list; } @Override public int getCount() { return list.size(); } @Override public Object getItem(int position) { return list.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(final int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = View.inflate(mContext, R.layout.list_item_goo, null); } ViewHolder holder = ViewHolder.getHolder(convertView); holder.mContent.setText(list.get(position)); //item固定小紅點layout LinearLayout pointLayout = holder.mPointLayout; //item固定小紅點 final TextView point = holder.mPoint; boolean visiable = !mRemoved.contains(position); pointLayout.setVisibility(visiable ? View.VISIBLE : View.GONE); if (visiable) { point.setText(String.valueOf(position)); pointLayout.setTag(position); GooViewListener mGooListener = new GooViewListener(mContext, pointLayout) { @Override public void onDisappear(PointF mDragCenter) { super.onDisappear(mDragCenter); mRemoved.add(position); notifyDataSetChanged(); Utils.showToast(mContext, "position " + position + " disappear."); } @Override public void onReset(boolean isOutOfRange) { super.onReset(isOutOfRange); notifyDataSetChanged();//刷新ListView Utils.showToast(mContext, "position " + position + " reset."); } }; //在point父布局內(nèi)的觸碰事件都進行監(jiān)聽 pointLayout.setOnTouchListener(mGooListener); } return convertView; } static class ViewHolder { public ImageView mImage; public TextView mPoint; public LinearLayout mPointLayout; public TextView mContent; public ViewHolder(View convertView) { mImage = (ImageView) convertView.findViewById(R.id.iv_head); mPoint = (TextView) convertView.findViewById(R.id.point); mPointLayout = (LinearLayout) convertView.findViewById(R.id.ll_point); mContent = (TextView) convertView.findViewById(R.id.tv_content); } public static ViewHolder getHolder(View convertView) { ViewHolder holder = (ViewHolder) convertView.getTag(); if (holder == null) { holder = new ViewHolder(convertView); convertView.setTag(holder); } return holder; } } }
由于listview需要知道GooView的狀態(tài),所以我們在GooView中增加一個接口,用于listView回調(diào)處理后續(xù)的邏輯。
interface OnDisappearListener { /** * GooView Disapper * * @param mDragCenter */ void onDisappear(PointF mDragCenter); /** * GooView onReset * * @param isOutOfRange */ void onReset(boolean isOutOfRange); }
新建一個實現(xiàn)了OnTouchListener以及OnDisappearListener 方法的的類,***將這個實現(xiàn)類設(shè)置給item中的紅點Layout。
public class GooViewListener implements OnTouchListener, OnDisappearListener { private WindowManager mWm; private WindowManager.LayoutParams mParams; private GooView mGooView; private View pointLayout; private int number; private final Context mContext; private Handler mHandler; public GooViewListener(Context mContext, View pointLayout) { this.mContext = mContext; this.pointLayout = pointLayout; this.number = (Integer) pointLayout.getTag(); mGooView = new GooView(mContext); mWm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); mParams = new WindowManager.LayoutParams(); mParams.format = PixelFormat.TRANSLUCENT;//使窗口支持透明度 mHandler = new Handler(mContext.getMainLooper()); } @Override public boolean onTouch(View v, MotionEvent event) { int action = MotionEventCompat.getActionMasked(event); // 當按下時,將自定義View添加到WindowManager中 if (action == MotionEvent.ACTION_DOWN) { ViewParent parent = v.getParent(); // 請求其父級View不攔截Touch事件 parent.requestDisallowInterceptTouchEvent(true); int[] points = new int[2]; //獲取pointLayout在屏幕中的位置(layout的左上角坐標) pointLayout.getLocationInWindow(points); //獲取初始小紅點中心坐標 int x = points[0] + pointLayout.getWidth() / 2; int y = points[1] + pointLayout.getHeight() / 2; // 初始化當前點擊的item的信息,數(shù)字及坐標 mGooView.setStatusBarHeight(Utils.getStatusBarHeight(v)); mGooView.setNumber(number); mGooView.initCenter(x, y); //設(shè)置當前GooView消失監(jiān)聽 mGooView.setOnDisappearListener(this); // 添加當前GooView到WindowManager mWm.addView(mGooView, mParams); pointLayout.setVisibility(View.INVISIBLE); } // 將所有touch事件轉(zhuǎn)交給GooView處理 mGooView.onTouchEvent(event); return true; } @Override public void onDisappear(PointF mDragCenter) { //disappear 下一步完成 } @Override public void onReset(boolean isOutOfRange) { // 當dragPoint彈回時,去除該View,等下次ACTION_DOWN的時候再添加 if (mWm != null && mGooView.getParent() != null) { mWm.removeView(mGooView); } } }
這樣下來,我們基本上完成了大部分功能,現(xiàn)在還差***一步,就是GooView超出范圍消失后的處理,這里我們用一個幀動畫來完成爆炸效果。
public class BubbleLayout extends FrameLayout { Context context; public BubbleLayout(Context context) { super(context); this.context = context; } private int mCenterX, mCenterY; public void setCenter(int x, int y) { mCenterX = x; mCenterY = y; requestLayout(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { View child = getChildAt(0); // 設(shè)置View到指定位置 if (child != null && child.getVisibility() != GONE) { final int width = child.getMeasuredWidth(); final int height = child.getMeasuredHeight(); child.layout((int) (mCenterX - width / 2.0f), (int) (mCenterY - height / 2.0f) , (int) (mCenterX + width / 2.0f), (int) (mCenterY + height / 2.0f)); } } } @Override public void onDisappear(PointF mDragCenter) { if (mWm != null && mGooView.getParent() != null) { mWm.removeView(mGooView); //播放氣泡爆炸動畫 ImageView imageView = new ImageView(mContext); imageView.setImageResource(R.drawable.anim_bubble_pop); AnimationDrawable mAnimDrawable = (AnimationDrawable) imageView .getDrawable(); final BubbleLayout bubbleLayout = new BubbleLayout(mContext); bubbleLayout.setCenter((int) mDragCenter.x, (int) mDragCenter.y - Utils.getStatusBarHeight(mGooView)); bubbleLayout.addView(imageView, new FrameLayout.LayoutParams( android.widget.FrameLayout.LayoutParams.WRAP_CONTENT, android.widget.FrameLayout.LayoutParams.WRAP_CONTENT)); mWm.addView(bubbleLayout, mParams); mAnimDrawable.start(); // 播放結(jié)束后,刪除該bubbleLayout mHandler.postDelayed(new Runnable() { @Override public void run() { mWm.removeView(bubbleLayout); } }, 501); } }
關(guān)于Android中怎么自定義控件問題的解答就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關(guān)注億速云行業(yè)資訊頻道了解更多相關(guān)知識。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。