先上效果图:

这里写图片描述这里写图片描述 

哈哈, 说下实现思路:
我们可以把每一个树桩, 小猪, 车厢都看成是一个Drawable, 这个Drawable里面保存了x, y坐标, 我们的SurfaceView在draw的时候, 就把这些Drawable draw出来.

那可能有的小伙伴就会问了:
1. 那小猪是怎么让它跑起来, 并且腿部还不断地在动呢?
2. 还有小猪是怎么找到出路的呢?

刚刚我们讲过小猪是Drawable, 其实我们自定义的这个Drawable就是一个帧动画, 它里面有一个Bitmap数组, 一个currentIndex(这个用来记录当前帧), 我们在子线程里面不断更新这个currentIndex, 当Drawable被调用draw的时候, 就根据currentIndex来从Bitmap数组里面取对应的bitmap出来. 刚刚还讲过Drawable里面保存了当前x, y坐标, 我们的路径动画在播放的时候, 就不断的更新里面的坐标, 另外, SurfaceView那边也不断的调用这些Drawable的draw方法, 把他们画出来, 这样小猪就可以边移动, 边播放奔跑的动画了, 哈哈.

小猪找出路的话, 我们先看看这个:

这里写图片描述 

哈哈哈, 这样思路是不是清晰了好多.
其实我们的SurfaceView里面有一个Rect二维数组, 用来存放这些矩形, 小猪离开手指之后, 就开始从小猪当前所在的矩形,
用广度优先遍历, 找到一条最短的路径(比如: [5,5 5,4 5,3 5,2 5,1 5,0]这样的), 然后再根据这条路径在Rect数组中找到对应的矩形, 最后根据这些对应的矩形的坐标来确定出Path.
哈哈, 有了Path小猪就可以跑了.

下面我们先来看看那个自定义的Drawable怎么写 (下面的那个ThreadPool类就是我们自己封装的一个单例的线程池):

/**
 * 自定义的Drawable,类似于AnimationDrawable
 */
public class MyDrawable extends Drawable implements Cloneable {

    private final int mDelay;//帧延时
    private final byte[] mLock;//控制线程暂停的锁
    private Semaphore mSemaphore;//来用控制线程更新问题
    private Bitmap[] mBitmaps;//帧
    private Paint mPaint;
    private int mCurrentIndex;//当前帧索引
    private float x, y;//当前坐标
    private Future mTask;//帧动画播放的任务
    private volatile boolean isPaused;//已暂停

    public MyDrawable(int delay, Bitmap... bitmaps) {
        mSemaphore = new Semaphore(1);
        mBitmaps = bitmaps;
        mDelay = delay;
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mLock = new byte[0];
    }

    public void start() {
        stop();
        mTask = ThreadPool.getInstance().execute(() -> {
            while (true) {
                synchronized (mLock) {
                    while (isPaused) {
                        try {
                            mLock.wait();
                        } catch (InterruptedException e) {
                            return;
                        }
                    }
                }
                try {
                    Thread.sleep(mDelay);
                } catch (InterruptedException e) {
                    return;
                }
                try {
                    mSemaphore.acquire();
                } catch (InterruptedException e) {
                    return;
                }
                mCurrentIndex  ;
                if (mCurrentIndex == mBitmaps.length) {
                    mCurrentIndex = 0;
                }
                mSemaphore.release();
            }
        });
    }

    void pause() {
        isPaused = true;
    }

    void resume() {
        isPaused = false;
        synchronized (mLock) {
            mLock.notifyAll();
        }
    }

    private void stop() {
        if (mTask != null) {
            mTask.cancel(true);
            mTask = null;
            mCurrentIndex = 0;
        }
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        try {
            mSemaphore.acquire();
        } catch (InterruptedException e) {
            return;
        }
        canvas.drawBitmap(mBitmaps[mCurrentIndex], x, y, mPaint);
        mSemaphore.release();
    }

    public void release() {
        stop();
        if (mBitmaps != null) {
            for (Bitmap bitmap : mBitmaps) {
                if (bitmap != null && !bitmap.isRecycled()) {
                    bitmap.recycle();
                }
            }
        }
        mBitmaps = null;
        mPaint = null;
        mTask = null;
    }

    public float getX() {
        return x;
    }

    public void setX(float x) {
        this.x = x;
    }

    public float getY() {
        return y;
    }

    public void setY(float y) {
        this.y = y;
    }

    public Bitmap getBitmap() {
        Bitmap result = null;
        if (mBitmaps != null && mBitmaps.length > 0) {
            result = mBitmaps[0];
        }
        return result;
    }

    @Override
    public int getIntrinsicWidth() {
        if (mBitmaps.length == 0) {
            return 0;
        }
        return mBitmaps[0].getWidth();
    }

    @Override
    public int getIntrinsicHeight() {
        if (mBitmaps.length == 0) {
            return 0;
        }
        return mBitmaps[0].getHeight();
    }

    @Override
    public void setAlpha(int alpha) {
        mPaint.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(ColorFilter cf) {
        mPaint.setColorFilter(cf);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    @SuppressWarnings("MethodDoesntCallSuperMethod")
    public MyDrawable clone() {
        return new MyDrawable(0, mBitmaps[0]);
    }
}

start方法大概就是开启一个子线程, 每次指定延时过后就更新currentIndex, currentIndex超出范围就置0, 这样就可以一直循环播放下去了, 哈哈.
mSemaphore是当执行draw的时候, 用来锁定currentIndex不让更新的.

好了, 现在有了Drawable, 我们再来看看Path是怎么播放的:
我们可以先获取到Path上面的点, 有了这些点接下来就非常简单了.
获取Path上面的点坐标的方法大家应该也很熟悉了吧, 代码就不贴出来了,5.0及以上系统用Path的approximate方法, 5.0系统以下的用PathMeasure类. 具体代码在SDK里面也可以找到.
播放Path的话, 我们可以自定义一个PathAnimation:
其实我们自定义的这个PathAnimation播放Path的逻辑也非常简单:当start方法执行的时候,记录一下开始时间,然后一个while循环,条件就是: 当前时间 - 开始时间 < 动画时长, 然后根据当前动画已经播放的时长和总动画时长计算出当前动画的播放进度, 然后我们就可以用这个progress来获取Path上对应的点,看看完整的代码:

public class PathAnimation {

    private Keyframes mPathKeyframes;//关键帧
    private long mAnimationDuration;//动画时长
    private OnAnimationUpdateListener mOnAnimationUpdateListener;//动画更新监听
    private AnimationListener mAnimationListener;//动画事件监听
    private volatile boolean isAnimationRepeat, //反复播放的动画
            isAnimationStopped,//已停止
            isAnimationCanceled, //已取消 (停止和取消的区别: 取消是在动画播放完之前主动取消的,  停止是动画播放完,自动停止的)
            isAnimationEndListenerCalled;//动画已取消的监听已经回调

    PathAnimation(MyPath path) {
        updatePath(path);
    }

    public PathAnimation setDuration(long duration) {
        mAnimationDuration = duration;
        return this;
    }

    void updatePath(MyPath path) {
        //根据系统版本选择更合适的关键帧类
        mPathKeyframes = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? new PathKeyframes(path) : new PathKeyframesSupport(path);
    }

    OnAnimationUpdateListener getUpdateListener() {
        return mOnAnimationUpdateListener;
    }

    void setUpdateListener(OnAnimationUpdateListener listener) {
        mOnAnimationUpdateListener = listener;
    }

    /**
     * 设置动画是否重复播放
     */
    public PathAnimation setRepeat(boolean isAnimationRepeat) {
        this.isAnimationRepeat = isAnimationRepeat;
        return this;
    }

    boolean isAnimationRepeat() {
        return isAnimationRepeat;
    }

    AnimationListener getAnimationListener() {
        return mAnimationListener;
    }

    void setAnimationListener(AnimationListener listener) {
        mAnimationListener = listener;
    }

    void start() {
        if (mAnimationDuration > 0) {
            ThreadPool.getInstance().execute(() -> {
                isAnimationStopped = false;
                isAnimationCanceled = false;
                isAnimationEndListenerCalled = false;
                final long startTime = SystemClock.uptimeMillis();
                long currentPlayedDuration;//当前动画已经播放的时长
                if (mAnimationListener != null) {
                    mAnimationListener.onAnimationStart();
                }
                while ((currentPlayedDuration = SystemClock.uptimeMillis() - startTime) < mAnimationDuration) {
                    //如果动画被打断则跳出循环
                    if (isAnimationInterrupted()) {
                        break;
                    }
                    //根据当前动画已经播放的时长和总动画时长计算出当前动画的播放进度
                    float progress = (float) currentPlayedDuration / (float) mAnimationDuration;
                    if (mOnAnimationUpdateListener != null) {
                        if (!isAnimationInterrupted()) {
                            mOnAnimationUpdateListener.onUpdate(progress, mPathKeyframes.getValue(progress));
                        }
                    }
                }
                if (isAnimationRepeat && !isAnimationInterrupted()) {
                    //如果是设置了重复并且还没有被取消,则重复播放动画
                    mPathKeyframes.reverse();
                    if (mAnimationListener != null) {
                        mAnimationListener.onAnimationRepeat();
                    }
                    start();
                } else {
                    isAnimationStopped = true;
                    if (mAnimationListener != null) {
                        //判断应该回调哪一个接口
                        if (isAnimationCanceled) {
                            mAnimationListener.onAnimationCanceled();
                        } else {
                            mAnimationListener.onAnimationEnd();
                        }
                    }
                    //标记接口已回调
                    isAnimationEndListenerCalled = true;
                }
            });
        }
    }

    /**
     会阻塞,直到动画真正停止才返回
     */
    private void waitStopped() {
        isAnimationStopped = true;
        //noinspection StatementWithEmptyBody
        while (!isAnimationEndListenerCalled) {
        }
    }

    /**
     会阻塞,直到动画真正取消才返回
     */
    private void waitCancel(){
        isAnimationCanceled = true;
        //noinspection StatementWithEmptyBody
        while (!isAnimationEndListenerCalled) {
        }
    }

    void stop() {
        waitStopped();
    }

    void cancel() {
        waitCancel();
    }

    /**
     动画被打断
     */
    private boolean isAnimationInterrupted() {
        return isAnimationCanceled || isAnimationStopped;
    }

    public interface OnAnimationUpdateListener {
        void onUpdate(float currentProgress, PointF position);
    }

    public interface AnimationListener {
        void onAnimationStart();//动画开始

        void onAnimationEnd();//动画结束

        void onAnimationCanceled();//动画取消

        void onAnimationRepeat();//动画重复播放
    }
}

我们通过setUpdateListener方法来监听动画进度, OnAnimationUpdateListener接口的onUpdate方法参数还有一个PointF, 这个PointF就是根据动画当前进度从mPathKeyframes中获取到Path所对应的坐标点.

我们来写一个demo来看看这个PathAnimation的效果:
一
哈哈, 可以了, 是我们想要的效果.

现在动画什么的都准备好了,就差怎么把出路变成Path了,我们先来看看怎么找出路:
上面说到,屏幕上都铺满了矩形,我们可以再创建一个int类型的二维数组,用来保存这些矩形的状态(空闲:0,小猪占用:1,树桩占用:2)
我们把这个数组打印出来是这样的:

    0   0   0   0   0   0   0   0   0
      0   0   0   0   0   0   0   0   0
    0   0   0   0   0   0   0   0   0
      0   0   0   0   0   0   0   0   0
    0   0   0   0   0   0   0   0   0
      0   0   0   0   0   0   0   0   0
    0   0   0   0   0   0   0   0   0
      0   0   0   0   0   0   0   0   0
    0   0   0   0   0   0   0   0   0

我们再看看这个数组:(空闲:0,小猪占用:1,树桩占用:2)

    0   0   0   0   0   0   0   0   0
      0   0   0   0   0   0   0   0   0
    0   0   0   0   2   2   0   0   0
      0   0   0   2   1   2   0   0   0
    0   0   0   2   0   2   0   0   0
      2   0   2   0   2   0   0   0   0
    2   0   0   0   0   0   0   0   0
      0   2   2   2   2   0   0   0   0
    0   2   0   0   0   0   0   0   0

这时候就要用到一个 “广度优先遍历” ,思路就是 (逻辑有点复杂,一次看不懂看多几次就明白了):

    先传入这个状态数组和当前小猪的坐标;
    创建一个List<List<Point>>,存放出路,名字就叫做footprints吧;
    创建一个队列,这个队列存放待查找的点;
    当前小猪的坐标先放进footprints;
    当前小猪的坐标入队;
    标记小猪坐标已经走过;
    进入循环 (循环条件就是队列不为空){
        创建一个临时的footprints;(因为最多可能有6个新的路径)
        队头出队;
        寻找周围6个方向(上,下,左,右,左上,右上,左下,右下)可以到达的位置 (不包括越界的、标记过的、不是空闲的);
        遍历这个可到达位置的数组{
            遍历footprints{
                检查队头的坐标是否跟footprints的元素(List<Point>)的最后一个元素(Point)的位置(x,y)是一样的(即可以链接)
                (比如: 现在footprints是[[(5,5), (5,4)], [(6,5), (6,4)]],队头的坐标是(5,4), 那可达位置的数组就可能是[(5,3), (5,5), (4,4), (4,5), (6,4), (6,5)]){
                    则创建一个新的List<Point>;
                    add footprints的元素(比如: [(5,5), (5,4)]);
                    再add可达位置的坐标(比如: (5,3);
                    临时的footprints add 这个新的List (那临时的footprints现在就是 [[(5,5), (5,4), (5,3)]]了);
                }                 
            }
            检查本次可达位置的坐标是否已经是在边界 (已经找到出路){
                footprints add 临时的footprints的元素;
                遍历footprints{
                    判断footprints的元素的最后一位是否边界位置{
                        return 这个footprints的元素; (必然是最短的路径);
                    }
                }
            }
            队列入队本次可达位置的坐标;
        }
        (本次没有找到出路) footprints addAll 临时的footprints的元素,准备下一轮循环;
    }
    执行到了这里, 即表示没有出路, 如果footprints里面是空的话,我们直接返回null,如果不为空,就返回footprints最后一个元素,即能走的最长的一条路径;

好了,我们看看代码是怎么写的 (WayData等同于Point, 里面也保存有x, y坐标点):

    public static List<WayData> findWay(int[][] items, WayData currentPos) {
        //获取数组的尺寸
        int verticalCount = items.length;
        int horizontalCount = items[0].length;
        //创建队列
        Queue<WayData> way = new ArrayDeque<>();
        //出路
        List<List<WayData>> footprints = new ArrayList<>();
        //复制一个新的数组 (因为要标记状态)
        int[][] pattern = new int[verticalCount][horizontalCount];
        for (int vertical = 0; vertical < verticalCount; vertical  ) {
            System.arraycopy(items[vertical], 0, pattern[vertical], 0, horizontalCount);
        }
        //当前坐标入队
        way.offer(currentPos);
        //添加进集合
        List<WayData> temp = new ArrayList<>();
        temp.add(currentPos);
        footprints.add(temp);
        //标记状态 (已走过)
        pattern[currentPos.y][currentPos.x] = STATE_WALKED;

        while (!way.isEmpty()) {
            //队头出队
            WayData header = way.poll();
            //以header为中心,获取周围可以到达的点(即未被占用,未标记过的)(这个方法在获取到可到达的点时,会标记这个点为: 已走过)
            List<WayData> directions = getCanArrivePos(pattern, header);
            //创建临时的footprints
            List<List<WayData>> footprintsTemp = new ArrayList<>();
            //遍历可到达的点
            for (int i = 0; i < directions.size(); i  ) {
                WayData direction = directions.get(i);

                for (List<WayData> tmp : footprints) {
                    //检查是否可以链接
                    if (canLinks(header, tmp)) {
                        List<WayData> list = new ArrayList<>();
                        list.addAll(tmp);
                        list.add(direction);
                        footprintsTemp.add(list);
                    }
                }
                //检查是否已达到边界
                if (isEdge(verticalCount, horizontalCount, direction)) {
                    if (!footprintsTemp.isEmpty()) {
                        footprints.addAll(footprintsTemp);
                    }
                    //返回最短的出路
                    for (List<WayData> tmp : footprints) {
                        if (!tmp.isEmpty() && isEdge2(verticalCount, horizontalCount, tmp)) {
                            return tmp;
                        }
                    }
                }
                //本次未找到出路,入队这个可到达的点
                way.offer(direction);

            }
            //准备下一轮循环
            if (!footprintsTemp.isEmpty()) {
                footprints.addAll(footprintsTemp);
            }
        }
        //没有出路,返回能走的最长的一条路径;
        return footprints.isEmpty() ? null : footprints.get(footprints.size() - 1);
    }

getCanArrivePos方法:

    /**
     寻找周围6个方向可以到达的位置(不包括越界的,标记过的,不是空闲的)
     */
    public static List<WayData> getCanArrivePos(int[][] items, WayData currentPos) {
        int verticalCount = items.length;
        int horizontalCount = items[0].length;
        List<WayData> result = new ArrayList<>();
        int offset = currentPos.y % 2 == 0 ? 0 : 1, offset2 = currentPos.y % 2 == 0 ? 1 : 0;
        for (int i = 0; i < 6; i  ) {
            WayData tmp = getNextPosition(currentPos, offset, offset2, i);
            if ((tmp.x > -1 && tmp.x < horizontalCount) && (tmp.y > -1 && tmp.y < verticalCount)) {
                if (items[tmp.y][tmp.x] != Item.STATE_SELECTED && items[tmp.y][tmp.x] != Item.STATE_OCCUPIED && items[tmp.y][tmp.x] != STATE_WALKED) {
                    result.add(tmp);
                    items[tmp.y][tmp.x] = STATE_WALKED;
                }
            }
        }
        //打乱它,为了让方向顺序不一样, 即每次都不同
        Collections.shuffle(result);
        return result;
    } 

getNextPosition方法:

    /**
     根据当前方向获取对应的位置
     */
    private static WayData getNextPosition(WayData currentPos, int offset, int offset2, int direction) {
        WayData result = new WayData(currentPos.x, currentPos.y);
        switch (direction) {
            case 0:
                //左
                result.x -= 1;
                break;
            case 1:
                //左上
                result.x -= offset;
                result.y -= 1;
                break;
            case 2:
                //左下
                result.x -= offset;
                result.y  = 1;
                break;
            case 3:
                //右
                result.x  = 1;
                break;
            case 4:
                //右上
                result.x  = offset2;
                result.y -= 1;
                break;
            case 5:
                //右下
                result.x  = offset2;
                result.y  = 1;
                break;
        }
        return result;
    }

我们执行findWay方法,就会得到这个结果:

    0   0   0   0   0   0   0   0   0
      0   0   0   0   0   0   0   0   0
    0   0   0   0   2   2   0   0   0
      0   0   0   2   1   2   0   0   0
    0   0   0   2   *   2   0   0   0
      2   0   2   *   2   0   0   0   0
    2   *   *   *   0   0   0   0   0
      *   2   2   2   2   0   0   0   0
    0   2   0   0   0   0   0   0   0

哈哈,是不是很好玩, 我们将这条出路的坐标,分别获取到对应的Rect,再根据这个Rect的坐标来连接成Path, 然后我们的小猪就可以跑啦.

本文到此结束,有错误的地方请指出,谢谢大家!

SurfaceView开发[捉小猪]手机游戏 (二)

完整代码地址: https://github.com/wuyr/CatchPiggy

游戏主页: https://wuyr.github.io/

到此这篇关于SurfaceView开发[捉小猪]手机游戏 (一)的文章就介绍到这了,更多相关SurfaceView游戏内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

SurfaceView开发[捉小猪]手机游戏 (一)的更多相关文章

  1. html5 canvas合成海报所遇问题及解决方案总结

    这篇文章主要介绍了html5 canvas合成海报所遇问题及解决方案总结,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Html5 video标签视频的最佳实践

    这篇文章主要介绍了Html5 video标签视频的最佳实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  3. HTML5在微信内置浏览器下右上角菜单的调整字体导致页面显示错乱的问题

    HTML5在微信内置浏览器下,在右上角菜单的调整字体导致页面显示错乱的问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧

  4. ios – containerURLForSecurityApplicationGroupIdentifier:在iPhone和Watch模拟器上给出不同的结果

    我使用默认的XCode模板创建了一个WatchKit应用程序.我向iOSTarget,WatchkitAppTarget和WatchkitAppExtensionTarget添加了应用程序组权利.(这是应用程序组名称:group.com.lombax.fiveminutes)然后,我尝试使用iOSApp和WatchKitExtension访问共享文件夹URL:延期:iOS应用:但是,测试NSURL

  5. Ionic – Splash Screen适用于iOS,但不适用于Android

    我有一个离子应用程序,其中使用CLI命令离子资源生成的启动画面和图标iOS版本与正在渲染的启动画面完美配合,但在Android版本中,只有在加载应用程序时才会显示白屏.我检查了config.xml文件,所有路径看起来都是正确的,生成的图像出现在相应的文件夹中.(我使用了splash.psd模板来生成它们.我错过了什么?这是config.xml文件供参考,我觉得我在这里做错了–解决方法在config.xml中添加以下键:它对我有用!

  6. ios – 无法启动iPhone模拟器

    /Library/Developer/CoreSimulator/Devices/530A44CB-5978-4926-9E91-E9DBD5BFB105/data/Containers/Bundle/Application/07612A5C-659D-4C04-ACD3-D211D2830E17/ProductName.app/ProductName然后,如果您在Xcode构建设置中选择标准体系结构并再次构建和运行,则会产生以下结果:dyld:lazysymbolbindingFailed:Symbol

  7. Xamarin iOS图像在Grid内部重叠

    heyo,所以在Xamarin我有一个使用并在其中包含一对,所有这些都包含在内.这在Xamarin.Android中看起来完全没问题,但是在Xamarin.iOS中,图像与标签重叠.我不确定它的区别是什么–为什么它在Xamarin.Android中看起来不错但在iOS中它的全部都不稳定?

  8. 在iOS上向后播放HTML5视频

    我试图在iPad上反向播放HTML5视频.HTML5元素包括一个名为playbackRate的属性,它允许以更快或更慢的速率或相反的方式播放视频.根据Apple’sdocumentation,iOS不支持此属性.通过每秒多次设置currentTime属性,可以反复播放,而无需使用playbackRate.这种方法适用于桌面Safari,但似乎在iOS设备上的搜索限制为每秒1次更新–在我的情况下太慢了.有没有办法在iOS设备上向后播放HTML5视频?解决方法iOS6Safari现在支持playbackRat

  9. 使用 Swift 语言编写 Android 应用入门

    Swift标准库可以编译安卓armv7的内核,这使得可以在安卓移动设备上执行Swift语句代码。做梦,虽然Swift编译器可以胜任在安卓设备上编译Swift代码并运行。这需要的不仅仅是用Swift标准库编写一个APP,更多的是你需要一些框架来搭建你的应用用户界面,以上这些Swift标准库不能提供。简单来说,构建在安卓设备上使用的Swiftstdlib需要libiconv和libicu。通过命令行执行以下命令:gitclonegit@github.com:SwiftAndroid/libiconv-libi

  10. Android – 调用GONE然后VISIBLE使视图显示在错误的位置

    我有两个视图,A和B,视图A在视图B上方.当我以编程方式将视图A设置为GONE时,它将消失,并且它正下方的视图将转到视图A的位置.但是,当我再次将相同的视图设置为VISIBLE时,它会在视图B上显示.我不希望这样.我希望视图B回到原来的位置,这是我认为会发生的事情.我怎样才能做到这一点?编辑–代码}这里是XML:解决方法您可以尝试将两个视图放在RelativeLayout中并相对于彼此设置它们的位置.

随机推荐

  1. Flutter 网络请求框架封装详解

    这篇文章主要介绍了Flutter 网络请求框架封装详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Android单选按钮RadioButton的使用详解

    今天小编就为大家分享一篇关于Android单选按钮RadioButton的使用详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧

  3. 解决android studio 打包发现generate signed apk 消失不见问题

    这篇文章主要介绍了解决android studio 打包发现generate signed apk 消失不见问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

  4. Android 实现自定义圆形listview功能的实例代码

    这篇文章主要介绍了Android 实现自定义圆形listview功能的实例代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  5. 详解Android studio 动态fragment的用法

    这篇文章主要介绍了Android studio 动态fragment的用法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  6. Android用RecyclerView实现图标拖拽排序以及增删管理

    这篇文章主要介绍了Android用RecyclerView实现图标拖拽排序以及增删管理的方法,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下

  7. Android notifyDataSetChanged() 动态更新ListView案例详解

    这篇文章主要介绍了Android notifyDataSetChanged() 动态更新ListView案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下

  8. Android自定义View实现弹幕效果

    这篇文章主要为大家详细介绍了Android自定义View实现弹幕效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  9. Android自定义View实现跟随手指移动

    这篇文章主要为大家详细介绍了Android自定义View实现跟随手指移动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Android实现多点触摸操作

    这篇文章主要介绍了Android实现多点触摸操作,实现图片的放大、缩小和旋转等处理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部