如何高效、简单实现菜单拖拽排序?

作者:yechaoa

1、效果

2、简介

本文主角是ItemTouchHelper

它是RecyclerView对于item交互处理的一个「辅助类」,主要用于拖拽以及滑动处理。

以接口实现的方式,达到配置简单、逻辑解耦、职责分明的效果,并且支持所有的布局方式。

3、功能拆解

4、功能实现

4.1、实现接口

自定义一个类,实现ItemTouchHelper.Callback接口,然后在实现方法中根据需求简单配置即可。

class DragCallBack(adapter: DragAdapter,data: MutableList<String>) : ItemTouchHelper.Callback() {
}

ItemTouchHelper.Callback必须实现的3个方法:

  • • getMovementFlags
  • • onMove
  • • onSwiped

其他方法还有onSelectedChanged、clearView等

4.1.1、getMovementFlags

用于创建交互方式,交互方式分为两种:

  1. 1. 拖拽,网格布局支持上下左右,列表只支持上下(LEFT、UP、RIGHT、DOWN)

  2. 2. 滑动,只支持前后(START、END)

最后,通过makeMovementFlags把结果返回回去,makeMovementFlags接收两个参数,dragFlagsswipeFlags,即上面拖拽和滑动组合的标志位。

    override fun getMovementFlags(recyclerView: RecyclerView,viewHolder: RecyclerView.ViewHolder): Int {
        var dragFlags = 0
        var swipeFlags = 0
        when (recyclerView.layoutManager) {
            is GridLayoutManager -> {
                // 网格布局
                dragFlags = ItemTouchHelper.LEFT or ItemTouchHelper.UP or ItemTouchHelper.RIGHT or ItemTouchHelper.DOWN
                return makeMovementFlags(dragFlags,swipeFlags)
            }
            is LinearLayoutManager -> {
                // 线性布局
                dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
                swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
                return makeMovementFlags(dragFlags,swipeFlags)
            }
            else -> {
                // 其他情况可自行处理
                return 0
            }
        }
    }

4.1.2、onMove

拖拽时回调,这里我们主要对起始位置和目标位置的item做一个数据交换,然后刷新视图显示。

    override fun onMove(recyclerView: RecyclerView,viewHolder: RecyclerView.ViewHolder,target: RecyclerView.ViewHolder): Boolean {
        // 起始位置
        val fromPosition = viewHolder.adapterPosition
        // 结束位置
        val toPosition = target.adapterPosition
        // 固定位置
        if (fromPosition == mAdapter.fixedPosition || toPosition == mAdapter.fixedPosition) {
            return false
        }
        // 根据滑动方向 交换数据
        if (fromPosition < toPosition) {
            // 含头不含尾
            for (index in fromPosition until toPosition) {
                Collections.swap(mData,index,index + 1)
            }
        } else {
            // 含头不含尾
            for (index in fromPosition downTo toPosition + 1) {
                Collections.swap(mData,index - 1)
            }
        }
        // 刷新布局
        mAdapter.notifyItemMoved(fromPosition,toPosition)
        return true
    }

4.1.3、onSwiped

滑动时回调,这个回调方法里主要是做数据和视图的更新操作。

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder,direction: Int) {
        if (direction == ItemTouchHelper.START) {
            Log.i(TAG,"START--->向左滑")
        } else {
            Log.i(TAG,"END--->向右滑")
        }
        val position = viewHolder.adapterPosition
        mData.removeAt(position)
        mAdapter.notifyItemRemoved(position)
    }

4.2、绑定RecyclerView

上面接口实现部分我们已经简单写好了,逻辑也挺简单,总共不超过100行代码。

接下来就是把这个辅助类绑定到RecyclerView。

RecyclerView显示的实现就是基础的样式,就不展开了,可以查看源码

        val dragCallBack = DragCallBack(mAdapter,list)
        val itemTouchHelper = ItemTouchHelper(dragCallBack)
        itemTouchHelper.attachToRecyclerView(mBinding.recycleView)

绑定只需要调用attachToRecyclerView就好了。

至此,简单的效果就已经实现了。下面开始优化和进阶的部分。

4.3、设置分割线

RecyclerView网格布局实现等分,我们一般先是自定义ItemDecoration,然后调用addItemDecoration来实现的。

但是我在实现效果的时候遇到一个问题,因为我加了布局切换的功能,在每次切换的时候,针对不同的布局分别设置layoutManagerItemDecoration,这就导致随着切换次数的增加,item的间隔就越大。

addItemDecoration,顾名思义是添加,通过查看源码发现RecyclerView内部是有一个ArrayList来维护的,所以当我们重复调用addItemDecoration方法时,分割线是以递增的方式在增加的,并且在绘制的时候会从集合中遍历所有的分割线绘制。

部分源码:

    @Override
    public void draw(Canvas c) {
        super.draw(c);

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDrawOver(c,this,mState);
        }
        //...
    }

既然知道了问题所在,也大概想到了3种解决办法:

1.调用addItemDecoration前,先调用removeItemDecoration方法remove掉之前所有的分割线

2.调用addItemDecoration(@NonNull ItemDecoration decor,int index),通过index来维护

3. add时通过一个标示来判断,添加过就不添加了

好像可行,实际上并不太行…因为始终都有两个分割线实例。

我们再来梳理一下:

  • 两种不同的布局
  • 都有分割线
  • 分割线只需设置一次

我想到另外一个办法,不对RecyclerView做处理了,既然两种布局都有分割线,是不是可以把分割线合二为一了,然后根据LayoutManager去绘制不同的分割线?

理论上是可行的,事实上也确实可以…

自定义分割线:

class GridSpaceItemDecoration(private val spanCount: Int,private val spacing: Int = 20,private var includeEdge: Boolean = false) :
    RecyclerView.ItemDecoration() {

    override fun getItemOffsets(outRect: Rect,view: View,recyclerView: RecyclerView,state: RecyclerView.State) {
        recyclerView.layoutManager?.let {
            when (recyclerView.layoutManager) {
                is GridLayoutManager -> {
                    val position = recyclerView.getChildAdapterPosition(view) // 获取item在adapter中的位置
                    val column = position % spanCount // item所在的列
                    if (includeEdge) {
                        outRect.left = spacing - column * spacing / spanCount
                        outRect.right = (column + 1) * spacing / spanCount
                        if (position < spanCount) {
                            outRect.top = spacing
                        }
                        outRect.bottom = spacing
                    } else {
                        outRect.left = column * spacing / spanCount
                        outRect.right = spacing - (column + 1) * spacing / spanCount
                        if (position >= spanCount) {
                            outRect.top = spanCount
                        }
                        outRect.bottom = spacing
                    }
                }
                is LinearLayoutManager -> {
                    outRect.top = spanCount
                    outRect.bottom = spacing
                }
            }
        }
    }

}

4.4、选中放大/背景变色

为了提升用户体验,可以在拖拽的时候告诉用户当前拖拽的是哪个item,比如选中的item放大、背景高亮等。

  • 网格布局,选中变大

  • 列表布局,背景变色

这里用到ItemTouchHelper.Callback中的两个方法,onSelectedChangedclearView,我们需要在选中时改变视图显示,结束时再恢复。

4.4.1、onSelectedChanged

拖拽或滑动 发生改变时回调,这时我们可以修改item的视图

    override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?,actionState: Int) {
        if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
            viewHolder?.let {
                // 因为拿不到recyclerView,无法通过recyclerView.layoutManager来判断是什么布局,所以用item的宽度来判断
                // itemView.width > 500 用这个来判断是否是线性布局,实际取值自己看情况
                if (it.itemView.width > 500) {
                    // 线性布局 设置背景颜色
                    val drawable = it.itemView.background as GradientDrawable
                    drawable.color = ContextCompat.getColorStateList(it.itemView.context,R.color.greenDark)
                } else {
                    // 网格布局 设置选中放大
                    ViewCompat.animate(it.itemView).setDuration(200).scaleX(1.3F).scaleY(1.3F).start()
                }
            }
        }
        super.onSelectedChanged(viewHolder,actionState)
    }

actionState:

  • ACTION_STATE_IDLE 空闲状态
  • ACTION_STATE_SWIPE 滑动状态
  • ACTION_STATE_DRAG 拖拽状态

4.4.2、clearView

拖拽或滑动 结束时回调,这时我们要把改变后的item视图恢复到初始状态

    override fun clearView(recyclerView: RecyclerView,viewHolder: RecyclerView.ViewHolder) {
        // 恢复显示
        // 这里不能用if判断,因为GridLayoutManager是LinearLayoutManager的子类,改用when,类型推导有区别
        when (recyclerView.layoutManager) {
            is GridLayoutManager -> {
                // 网格布局 设置选中大小
                ViewCompat.animate(viewHolder.itemView).setDuration(200).scaleX(1F).scaleY(1F).start()
            }
            is LinearLayoutManager -> {
                // 线性布局 设置背景颜色
                val drawable = viewHolder.itemView.background as GradientDrawable
                drawable.color = ContextCompat.getColorStateList(viewHolder.itemView.context,R.color.greenPrimary)
            }
        }
        super.clearView(recyclerView,viewHolder)
    }

4.5、固定位置

在实际需求中,交互可能要求我们第一个菜单不可以变更顺序,只能固定,比如效果中的第一个菜单「推荐」固定在首位这种情况。

4.5.1、修改adapter

定义一个固定值,并设置不同的背景色和其他菜单区分开。

class DragAdapter(private val mContext: Context,private val mList: List<String>) : RecyclerView.Adapter<DragAdapter.ViewHolder>() {

    val fixedPosition = 0 // 固定菜单

    override fun onBindViewHolder(holder: ViewHolder,position: Int) {
        holder.mItemTextView.text = mList[position]

        // 第一个固定菜单
        val drawable = holder.mItemTextView.background as GradientDrawable
        if (holder.adapterPosition == 0) {
            drawable.color = ContextCompat.getColorStateList(mContext,R.color.greenAccent)
        }else{
            drawable.color = ContextCompat.getColorStateList(mContext,R.color.greenPrimary)
        }
    }
    //...
}

4.5.1、修改onMove回调

在onMove方法中判断,只要是固定位置就直接返回false。

class DragCallBack(adapter: DragAdapter,data: MutableList<String>) : ItemTouchHelper.Callback() {
    /**
     * 拖动时回调
     */
    override fun onMove(recyclerView: RecyclerView,target: RecyclerView.ViewHolder): Boolean {
        // 起始位置
        val fromPosition = viewHolder.adapterPosition
        // 结束位置
        val toPosition = target.adapterPosition

        // 固定位置
        if (fromPosition == mAdapter.fixedPosition || toPosition == mAdapter.fixedPosition) {
            return false
        }
        // ...
        return true
    }
}

虽然第一个菜单无法交换位置了,但是它还是可以拖拽的。

效果实现了吗,好像也实现了,可是又好像哪里不对,就好像填写完表单点击提交时你告诉我格式不正确一样,你不能一开始就告诉我吗?

为了进一步提升用户体验,可以让固定位置不可以拖拽吗?

可以,ItemTouchHelper.Callback中有两个方法:

  • isLongPressDragEnabled 是否可以长按拖拽
  • isItemViewSwipeEnabled 是否可以滑动

这俩方法默认都是true,所以即使不能交换位置,但默认也是支持操作的。

4.5.3、重写isLongPressDragEnabled

以拖拽举例,我们需要重写isLongPressDragEnabled方法把它禁掉,然后再非固定位置的时候去手动开启。

    override fun isLongPressDragEnabled(): Boolean {
        //return super.isLongPressDragEnabled()
        return false
    }

禁掉之后什么时候再触发呢?

因为我们现在的交互是长按进入编辑,那就需要在长按事件中再调用startDrag手动开启

        mAdapter.setOnItemClickListener(object : DragAdapter.OnItemClickListener {
            //...
            override fun onItemLongClick(holder: DragAdapter.ViewHolder) {
                if (holder.adapterPosition != mAdapter.fixedPosition) {
                    itemTouchHelper.startDrag(holder)
                }
            }
        })

ok,这样就完美实现了。

4.6、其他

4.6.1、position

因为有拖拽操作,下标其实是变化的,在做相应的操作时,要取实时位置

holder.adapterPosition

4.6.2、重置

不管是拖拽还是滑动,其实本质都是对Adapter内已填充的数据进行操作,实时数据通过Adapter获取即可。

如果想要实现重置功能,直接拿最开始的原始数据重新塞给Adapter即可。

Author:yechaoa

5、源码探索

看源码时,找对一个切入点,往往能达到事半功倍的效果。

这里就从绑定RecyclerView开始吧

        val dragCallBack = DragCallBack(mAdapter,list)
        val itemTouchHelper = ItemTouchHelper(dragCallBack)
        itemTouchHelper.attachToRecyclerView(mBinding.recycleView)

实例化ItemTouchHelper,然后调用其attachToRecyclerView方法绑定到RecyclerView。

5.1、attachToRecyclerView

    public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
        if (mRecyclerView == recyclerView) {
            return; // nothing to do
        }
        if (mRecyclerView != null) {
            destroyCallbacks();
        }
        mRecyclerView = recyclerView;
        if (recyclerView != null) {
            final Resources resources = recyclerView.getResources();
            mSwipeEscapeVelocity = resources.getDimension(R.dimen.item_touch_helper_swipe_escape_velocity);
            mMaxSwipeVelocity = resources.getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity);
            setupCallbacks();
        }
    }

这段代码其实有点意思的,解读一下:

  1. 第一个if判断,避免重复操作,直接return
  2. 第二个if判断,调用了destroyCallbacks,在destroyCallbacks里面做了一些移除和回收操作,说明只能绑定到一个RecyclerView;同时,注意这里判断的主体是mRecyclerView,不是我们传进来的recyclerView,而且我们传进来的recyclerView是支持Nullable的,所以我们可以传个空值走到destroyCallbacks里来做解绑操作
  3. 第三个if判断,当我们传的recyclerView不为空时,调用setupCallbacks

5.2、setupCallbacks

    private void setupCallbacks() {
        ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
        mSlop = vc.getScaledTouchSlop();
        mRecyclerView.addItemDecoration(this);
        mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
        mRecyclerView.addOnChildAttachStateChangeListener(this);
        startGestureDetection();
    }

这个方法里已经大概可以看出内部实现原理了。

两个关键点:

  • addOnItemTouchListener
  • startGestureDetection

通过触摸手势识别来处理交互显示。

5.3、mOnItemTouchListener

    private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
        @Override
        public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,@NonNull MotionEvent event) {
            mGestureDetector.onTouchEvent(event);
            if (action == MotionEvent.ACTION_DOWN) {
                //...
                if (mSelected == null) {
                    if (animation != null) {
                        //...
                        select(animation.mViewHolder,animation.mActionState);
                    }
                }
            } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
                select(null,ACTION_STATE_IDLE);
            } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
                //...
                if (index >= 0) {
                    checkSelectForSwipe(action,event,index);
                }
            }
            return mSelected != null;
        }

        @Override
        public void onTouchEvent(@NonNull RecyclerView recyclerView,@NonNull MotionEvent event) {
            mGestureDetector.onTouchEvent(event);
            //...
            if (activePointerIndex >= 0) {
                checkSelectForSwipe(action,activePointerIndex);
            }
            switch (action) {
                case MotionEvent.ACTION_MOVE: {
                    if (activePointerIndex >= 0) {
                        moveIfNecessary(viewHolder);
                    }
                    break;
                }
                //...
            }
        }

        @Override
        public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
            select(null,ACTION_STATE_IDLE);
        }
    };

这段代码删减之后还是有点多,不过没关系,提炼一下,核心通过判断MotionEvent调用了几个方法:

  • select
  • checkSelectForSwipe
  • moveIfNecessary

5.3.1、select

    void select(@Nullable ViewHolder selected,int actionState) {
        if (selected == mSelected && actionState == mActionState) {
            return;
        }
        //...
        if (mSelected != null) {
            if (prevSelected.itemView.getParent() != null) {
                final float targetTranslateX,targetTranslateY;
                switch (swipeDir) {
                    case LEFT:
                    case RIGHT:
                    case START:
                    case END:
                        targetTranslateY = 0;
                        targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth();
                        break;
                    //...
                }
                //...
            } else {
                removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
                mCallback.clearView(mRecyclerView,prevSelected);
            }
        }
        //...
        mCallback.onSelectedChanged(mSelected,mActionState);
        mRecyclerView.invalidate();
    }

这里面主要是在拖拽或滑动时对translateX/Y的计算和处理,然后通过mCallback.clearView和mCallback.onSelectedChanged回调给我们,最后调用invalidate()实时刷新。

5.3.2、checkSelectForSwipe

    void checkSelectForSwipe(int action,MotionEvent motionEvent,int pointerIndex) {
        //...
        if (absDx < mSlop && absDy < mSlop) {
            return;
        }
        if (absDx > absDy) {
            if (dx < 0 && (swipeFlags & LEFT) == 0) {
                return;
            }
            if (dx > 0 && (swipeFlags & RIGHT) == 0) {
                return;
            }
        } else {
            if (dy < 0 && (swipeFlags & UP) == 0) {
                return;
            }
            if (dy > 0 && (swipeFlags & DOWN) == 0) {
                return;
            }
        }
        select(vh,ACTION_STATE_SWIPE);
    }

这里是滑动处理的check,最后也是收敛到select()方法统一处理。

5.3.3、moveIfNecessary

    void moveIfNecessary(ViewHolder viewHolder) {
        if (mRecyclerView.isLayoutRequested()) {
            return;
        }
        if (mActionState != ACTION_STATE_DRAG) {
            return;
        }
        //...
        if (mCallback.onMove(mRecyclerView,viewHolder,target)) {
            // keep target visible
            mCallback.onMoved(mRecyclerView,fromPosition,target,toPosition,x,y);
        }
    }

这里检查拖拽时是否需要交换item,通过mCallback.onMoved回调给我们。

5.4、startGestureDetection

    private void startGestureDetection() {
        mItemTouchHelperGestureListener = new ItemTouchHelperGestureListener();
        mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),mItemTouchHelperGestureListener);
    }

5.4.1、ItemTouchHelperGestureListener

    private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
        //...
        @Override
        public void onLongPress(MotionEvent e) {
            //...
            View child = findChildView(e);
            if (child != null) {
                ViewHolder vh = mRecyclerView.getChildViewHolder(child);
                if (vh != null) {
                    //...
                    if (pointerId == mActivePointerId) {
                        //...
                        if (mCallback.isLongPressDragEnabled()) {
                            select(vh,ACTION_STATE_DRAG);
                        }
                    }
                }
            }
        }
    }

这里主要是对长按事件的处理,最后也是收敛到select()方法统一处理。

5.5、源码小结

  1. 1. 绑定RecyclerView

  2. 2. 注册触摸手势监听

  3. 3. 根据手势,先是内部处理各种校验、位置计算、动画处理、刷新等,然后回调给ItemTouchHelper.Callback

本质:主要工作都是源码帮我们做了,我们只需要在回调里根据结果处理业务逻辑即可。

原文地址:https://blog.csdn.net/weixin_61845324

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


更新Android SDK到3.0版本时,遇到Failed to rename directory E:\android\tools to E:\android\temp\ToolPackage.old01问题,导致无法更新,出现该问题的原因是由于3.0版本与较早的sdk版本之间文件结构有冲突,解决
Android 如何解决dialog弹出时无法捕捉Activity的back事件 在一些情况下,我们需要捕捉back键事件,然后在捕捉到的事件里写入我们需要进行的处理,通常可以采用下面三种办法捕捉到back事件: 1)重写onKeyDown或者onKeyUp方法 2)重写onBackPressed方
Android实现自定义带文字和图片的Button 在Android开发中经常会需要用到带文字和图片的button,下面来讲解一下常用的实现办法。一.用系统自带的Button实现 最简单的一种办法就是利用系统自带的Button来实现,这种方式代码量最小。在Button的属性中有一个是drawable
Android中的&quot;Unable to start activity ComponentInfo&quot;的错误 最近在做一款音乐播放器的时候,然后在调试的过程中发现一直报这个错误&quot;Unable to start activity ComponentInfo&quot;,从字面
Android 关于长按back键退出应用程序的实现最近在做一个Android上的应用,碰到一个问题就是如何实现长按back键退出应用程序。在网上查找了很多资料,发现几乎没有这样的实现,大部分在处理时是双击back键来退出应用程序。参考了一下双击back键退出应用程序的代码,网上主流的一种方法是下面
android自带的时间选择器只能精确到分,但是对于某些应用要求选择的时间精确到秒级,此时只有自定义去实现这样的时间选择器了。下面介绍一个可以精确到秒级的时间选择器。 先上效果图: 下面是工程目录: 这个控件我也是用的别人的,好像是一个老外写的,com.wheel中的WheelView是滑动控件的主
Android平台下利用zxing实现二维码开发 现在走在大街小巷都能看到二维码,而且最近由于项目需要,所以研究了下二维码开发的东西,开源的二维码扫描库主要有zxing和zbar,zbar在iPos平台上应用比较成熟,而在Android平台上主流还是用zxing库,因此这里主要讲述如何利用zxing
Android ListView的item背景色设置以及item点击无响应等相关问题 在Android开发中,listview控件是非常常用的控件,在大多数情况下,大家都会改掉listview的item默认的外观,下面讲解以下在使用listview时最常见的几个问题。1.如何改变item的背景色和按
如何向Android模拟器中导入含有中文名称的文件在进行Android开发的时候,如果需要向Android模拟器中导入文件进行测试,通过DDMS下手动导入或者在命令行下通过adb push命令是无法导入含有中文文件名的文件的。后来发现借用其他工具可以向模拟器中导入中文名称的文件,这个工具就是Ultr
Windows 下搭建Android开发环境一.下载并安装JDK版本要求JDK1.6+,下载JDK成功后进行安装,安装好后进行环境变量的配置【我的电脑】-——&gt;【属性】——&gt;【高级】 ——&gt;【环境变量】——&gt;【系统变量】中点击【新建】:变量名:CLASSPATH变量值:……
如何利用PopupWindow实现弹出菜单并解决焦点获取以及与软键盘冲突问题 在android中有时候可能要实现一个底部弹出菜单,此时可以考虑用PopupWindow来实现。下面就来介绍一下如何使用PopupWindow实现一个弹出窗。 主Activity代码:public void onCreat
解决Android中的ERROR: the user data image is used by another emulator. aborting的方法 今天调试代码的时候,突然出现这个错误,折腾了很久没有解决。最后在google上找到了大家给出的两种解决方案,下面给出这两种方法的链接博客:ht
AdvserView.java package com.earen.viewflipper; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory;
ImageView的scaleType的属性有好几种,分别是matrix(默认)、center、centerCrop、centerInside、fitCenter、fitEnd、fitStart、fitXY。 |值|说明| |:--:|:--| |center|保持原图的大小,显示在ImageVie
文章浏览阅读8.8k次,点赞9次,收藏20次。本文操作环境:win10/Android studio 3.21.环境配置 在SDK Tools里选择 CMAKE/LLDB/NDK点击OK 安装这些插件. 2.创建CMakeLists.txt文件 在Project 目录下,右键app,点击新建File文件,命名为CMakeLists.txt点击OK,创建完毕! 3.配置文件 在CMa..._link c++ project with gradle
文章浏览阅读1.2w次,点赞15次,收藏69次。实现目的:由mainActivity界面跳转到otherActivity界面1.写好两个layout文件,activity_main.xml和otherxml.xmlactivity_main.xml&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;RelativeLayout ="http://schemas..._android studio 界面跳转
文章浏览阅读3.8w次。前言:最近在找Android上的全局代理软件来用,然后发现了这两款神作,都是外国的软件,而且都是开源的软件,因此把源码下载了下来,给有需要研究代理这方面的童鞋看看。不得不说,国外的开源精神十分浓,大家相互使用当前基础的开源软件,然后组合成一个更大更强的大开源软件。好吧,废话不多说,下面简单介绍一下这两款开源项目。一、ProxyDroid:ProxyDroid功能比较强大,用到的技术也比较多,源码也_proxydroid
文章浏览阅读2.5w次,点赞17次,收藏6次。创建项目后,运行项目时Gradle Build 窗口却显示错误:程序包R不存在通常情况下是不会出现这个错误的。我是怎么遇到这个错误的呢?第一次创建项目,company Domain我使用的是:aven.com,但是创建过程在卡在了Building 'Calculator' Gradle Project info这个过程中,于是我选择了“Cancel”第二次创建项目,我还是使用相同的项目名称和项目路_r不存在
文章浏览阅读8.9w次,点赞4次,收藏43次。前言:在Android上使用系统自带的代理,限制灰常大,仅支持系统自带的浏览器。这样像QQ、飞信、微博等这些单独的App都不能使用系统的代理。如何让所有软件都能正常代理呢?ProxyDroid这个软件能帮你解决!使用方法及步骤如下:一、推荐从Google Play下载ProxyDroid,目前最新版本是v2.6.6。二、对ProxyDroid进行配置(基本配置:) (1) Auto S_proxydroid使用教程
文章浏览阅读1.1w次,点赞4次,收藏17次。Android Studio提供了一个很实用的工具Android设备监视器(Android device monitor),该监视器中最常用的一个工具就是DDMS(Dalvik Debug Monitor Service),是 Android 开发环境中的Dalvik虚拟机调试监控服务。可以进行的操作有:为测试设备截屏,查看特定进程中正在运行的线程以及堆栈信息、Logcat、广播状态信息、模拟电话_安卓摄像头调试工具