草庐IT

android - 安卓软键盘

coder 2023-11-27 原文

我正在为 android 2.2 及更高版本创建软键盘。一切都很好,但是当我输入得非常快时,有时我的 ACTION_DOWN 方法没有调用。被调用方法的实际流程应该是这样的

                    1) motionEvent.ACTION_DOWN
                    2) OnPress()   
                    3) motionEvent.ACTION_UP
                    4) OnRelease() and repeat same order for next word.

如果我以正常速度打字那么它工作正常但是如果我快速打字然后上面的方法执行顺序看起来像

                 1) motionEvent.ACTION_DOWN
                 2) OnPress() 
                 3) OnRelease()
                 4) motionEvent.ACTION_UP  and for next word OnPress and OnRelease() methods are being called.

有什么建议吗?

编辑 我的包含 MotionActionEvents 的 LatinKeyboardView 类

enter code here @Override
public boolean onTouchEvent(MotionEvent me) {
    // Moved next line and added lines to help solve reentrant problem.
    int action = me.getAction();
    // next 2 lines required for multitouch Andr 2+
    int act = action & MotionEvent.ACTION_MASK;
    final int ptrIndex = (act & MotionEvent.ACTION_POINTER_ID_MASK) //Renamed to ACTION_POINTER_INDEX_MASK in later Andro versions
    >> MotionEvent.ACTION_POINTER_ID_SHIFT;//Renamed to ACTION_POINTER_INDEX_SHIFT in later Andro versions

//      currentX = me.getX();
//      currentY = me.getY();
        calcMinSlide();

//      int act = me.getAction();
        if (act == android.view.MotionEvent.ACTION_DOWN) {
            Log.v(tag, "ANGLE_ACTION_DOWN : ");


        if (pw != null) {
            pw.dismiss();
            pw = null;
        }
        lastDirection = direction = 0;
        touchDownPoint.set(me.getX(), me.getY());

        // Will added next two lines
        touchDragPoint.set(me.getX(), me.getY());
        thresholdPoint.set(me.getX(), me.getY());
        // Will6 added to improve accuracy
        thresholdPoint1_5 = false;
        // Will7 added next 4 for Andro 2+
        currentX = me.getX();
        currentY = me.getY();
        // Save the ID of this first pointer (touch) down
        currentPointerID = me.getPointerId(0);
        nextPointerID = INVALID_POINTER_ID;

        previousDownTime = me.getEventTime();
        me.setLocation(touchDownPoint.x, touchDownPoint.y);
        // start timer on touch down
        startTimer(me, 300); // 150); Will7 changed this and removed method: checkLongPress

    } else if (act == android.view.MotionEvent.ACTION_UP
                || act == android.view.MotionEvent.ACTION_MOVE) {

        Log.v(tag, "ANGLE_ACTION_UP : ");
        //touchdragPoint and previoustouchPoint for calculating velocity
        PointF previousTouchPoint = new PointF(touchDragPoint.x,touchDragPoint.y);

        //Will7 added next if for Andro 2+: Find the index of the active pointer and fetch its position
        if (act == android.view.MotionEvent.ACTION_MOVE &&  me.getPointerId(ptrIndex) != currentPointerID) { 
            //Log.v(tag, "Cancel ATION_MOVE!! ID: "+me.getPointerId(ptrIndex));
            return super.onTouchEvent(me);
        }
        touchDragPoint.set(me.getX(), me.getY());           
        dy = me.getY() - touchDownPoint.y;
        dx = me.getX() - touchDownPoint.x;

        // added for Andro 2+
        currentX = touchDragPoint.x;
        currentY = touchDragPoint.y;

        //calculate time interval from down time to current time
        long timeInterval = me.getEventTime() - previousDownTime;
        previousDownTime = me.getEventTime();
        velocityThresDir = VELOCITY_THRESHOLD;
        float touchVelocity = Math.abs(distanceBetweenPoints(touchDragPoint, previousTouchPoint) / timeInterval);

        if (distanceFromCenter(dx,dy) > minSlide) {
//              Log.v(tag, "direction to detect angle....after... dx..."+dx+" dy "+dy);
                //Log.v(tag, "ANGLE angle.... after..."+distanceFromCenter(dx,dy)+" slide distance "+ minSlide);


            /* cancel the timer*/
            if (cDownTimer != null) {
                cDownTimer.cancel();
                cDownTimer = null;
            }
            /* coding for calculating velocity threshold*/          
            float angleThreshold = 0.0f;
            if ((thresholdPoint.x == touchDownPoint.x) && (thresholdPoint.y == touchDownPoint.y)){
                thresholdPoint.set(touchDragPoint.x, touchDragPoint.y);
            }
            else {
                //Will6 - added next if to improve accuracy
                if ((distanceFromCenter(dx,dy) > (minSlide * 1.5)) && !thresholdPoint1_5){
                    thresholdPoint.set(me.getX(),me.getY());
                    thresholdPoint1_5 = true;
                }
                float angleP1= calcAngle(touchDownPoint, thresholdPoint);
                float angleP2= calcAngle(previousTouchPoint, touchDragPoint);
                angleThreshold = Math.abs(angleP1 - angleP2);
                if (angleThreshold > Math.PI) angleThreshold = (float) (2.0 * Math.PI) - angleThreshold;
            }
//              velocityThresDir = (float) Math.abs((Math.cos(angleThreshold) * touchVelocity*1000));
                velocityThresDir = (float) (Math.cos(angleThreshold) * touchVelocity*1000);

            //end of calculation for velocity threshold 


            double angle = newM(touchDownPoint.x, touchDownPoint.y, touchDragPoint.x, touchDragPoint.y);
//              Log.v(tag, "ANGLE_FIRST_X "+touchDownPoint.x+"FIRST_Y "+touchDownPoint.y);
//              Log.v(tag, "ANGLE_SECOND_X "+touchDragPoint.x+"SECOND_Y "+touchDragPoint.y);
//              Log.v(tag, "ANGLE_FIRST"+angle);

            if ((touchDownPoint.x != thresholdPoint.x) || (touchDownPoint.y != thresholdPoint.y)) {
                double angleThresh = newM(touchDownPoint.x, touchDownPoint.y, thresholdPoint.x, thresholdPoint.y);
                double angleBetween = Math.abs(angle - angleThresh);
                if(angleBetween < 45 || angleBetween > 315){
                    if(angleBetween > 315) {
                        if (angle < angleThresh) {
                            angle += 360;
                        }
                        else if (angle > angleThresh) {
                            angleThresh += 360;
                        }
                        angle = Math.abs((angle - angleThresh)%360) / 2.0;
//                          Log.v(tag, "ANGLE_SECOND"+angle);
                        }
                        else {
                            angle = (angle + angleThresh * 1.0) / 2.0;
//                          Log.v(tag, "ANGLE_THIRD"+angle);
                        }
                    }
                }

            if (angle > 337.5){ 
                direction = 3;
            }else if (angle > 292.5){ 
                direction = 5;
            }else if (angle > 247.5){ 
                direction = 4;
            }else if (angle > 202.5){ 
                direction = 6;
            }else if (angle > 157.5){ 
                direction = 1;
            }else if (angle > 112.5){ 
                direction = 7;
            }else if (angle >  67.5){ 
                direction = 2;
            }else if (angle >  22.5){ 
                direction = 8;
            }else{
                direction = 3;
            }

            /* start timer if velocity is below velocity threshold*/    
            if ((velocityThresDir < VELOCITY_THRESHOLD) &&
                    (act == android.view.MotionEvent.ACTION_MOVE) && (cDownTimer == null) &&
                    (pw == null)) { //"&& cDownTimer" can be removed I think
                /* start timer with motionEvent and time in ms as a parameter */
                // added next two lines
                callOnLongPress(me);
                startTimerShowPopup(me,100);//Will changed from 150
             }          
        } else {
            direction = 0;
        }

        if (act == android.view.MotionEvent.ACTION_MOVE) {
            return true;
        } else if (act == android.view.MotionEvent.ACTION_UP) {
            if (cDownTimer != null) {
                cDownTimer.cancel();
                cDownTimer = null;
            }
            if (pw != null)
                pw.dismiss();
            if (longPressedKey) {
                SoftKeyboard.mComposing
                        .append(charset[mappedKey][direction]);
                popUpTextEntryScheme = true;
            }

            longPressedKey = false;
            currentPointerID = INVALID_POINTER_ID;
        }
    }

    else if (act == android.view.MotionEvent.ACTION_POINTER_DOWN) {
        //          if (me.getPointerCount() > 1) { //Should always be true, I think
            nextPointerID = me.getPointerId(ptrIndex);
            nextTouchDownPoint.set(me.getX(ptrIndex),me.getY(ptrIndex));
//          }
        } 
        else if (act == android.view.MotionEvent.ACTION_CANCEL) {
            currentPointerID = INVALID_POINTER_ID;
            nextPointerID  = INVALID_POINTER_ID;

    }
    else if (act == android.view.MotionEvent.ACTION_POINTER_UP) {
        // Extract the index of the pointer that left the touch sensor
        final int pointerId = me.getPointerId(ptrIndex);
        if (pointerId == currentPointerID) {
            // This was our active pointer going up. Choose a new
            // active pointer and adjust accordingly.
            final int newPointerIndex = ptrIndex == 0 ? 1 : 0;
            currentPointerID = nextPointerID;//(0);
            touchDownPoint.set(nextTouchDownPoint.x,nextTouchDownPoint.y);
            if (cDownTimer != null) {
                cDownTimer.cancel();
                cDownTimer = null;
            }
            if (pw != null) {
                pw.dismiss();
                pw = null;
            }
            if (longPressedKey) {
                SoftKeyboard.mComposing
                        .append(charset[mappedKey][direction]);
                popUpTextEntryScheme = true;
            }
            longPressedKey = false;
            lastDirection = direction = 0; // keysAtOnce=0;

            touchDragPoint.set(me.getX(newPointerIndex),me.getY(newPointerIndex));
            thresholdPoint.set(nextTouchDownPoint.x,nextTouchDownPoint.y);
            //added to improve accuracy
            thresholdPoint1_5 = false;
            // added next 3 for Andro 2+
            currentX = touchDragPoint.x;
            currentY = touchDragPoint.y;
            // Save the ID of this first pointer (touch) down

            previousDownTime = me.getEventTime();
            me.setLocation(touchDownPoint.x, touchDownPoint.y);
            //start timer on touch down     
            startTimer(me,300); //150); Will7 changed this and removed method: checkLongPress   
        } else { //Second pointer up before first. (Not handling 3 or more pointers yet!)

//              nextPointerID  = INVALID_POINTER_ID;
            }
        }   //else



    return super.onTouchEvent(me); // after we return here the service will get notified, etc
//      return true;
    }

和我的SoftKeyboard 类..

    public void onPress(int primaryCode) {
        Log.v("SoftKeyboard", "ANGLE_ACTION_ON_PRESS : ");

        //  added next section for repeating backspace
        if (RepeatBSTimer != null) {
            RepeatBSTimer.cancel();
            RepeatBSTimer = null;
        }
        if (mp != null) { // /Will7 moved this from just above keystroke
                            // statement
            mp.release();
            mp = null;
        }

        //  added for Andro 2+ multitouch
        if (primaryCode == pressedCode
                && LatinKeyboardView.nextPointerID != LatinKeyboardView.INVALID_POINTER_ID) {
            // I need to look up the real primaryCode here. (Not sure how!)
            // Android gives wrong values when touches overlap.
            wrongPrimaryCode = true;
            return;
        } else
            wrongPrimaryCode = false;

        pressedCode = primaryCode;

        //  added next section for repeating backspace
        if (primaryCode == Keyboard.KEYCODE_DELETE) {
            RepeatBSTimer = new CountDownTimer(1500000, 75) {
                @Override
                public void onTick(long millisUntilFinished) {
                    int primaryCode2;
                    if (LatinKeyboardView.longPressedKey
                            || (1500000 - millisUntilFinished > 500)) {
                        primaryCode2 = getCharFromKey(pressedCode,
                                LatinKeyboardView.direction, mInputView
                                        .getKeyboard());
                        if (primaryCode2 == Keyboard.KEYCODE_DELETE) {
                            repeating = true;
                            handleBackspace();
                        } else if (primaryCode2 == KEYCODE_DELETEWORD
                                && (millisUntilFinished % 150) < 75) {
                            repeating = true;
                            deleteLastWord();
                        }
                    }
                }

                @Override
                public void onFinish() {
                }
            };
            RepeatBSTimer.start();
        }
        //  added section for repeating backspace

        Uri uri = Uri.parse("android.resource://" + getPackageName() + "/"
                + R.raw.keystroke);// Play Key Click
        try {
            mp = new MediaPlayer();
            mp.setDataSource(this, uri);
            mp.prepare();
            mp.start();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
}

public void onRelease(int primaryCode) {

        // Will7 added next line if for Andro 2+ multitouch
        if (wrongPrimaryCode
                && LatinKeyboardView.nextPointerID != LatinKeyboardView.INVALID_POINTER_ID) {

            return;
        }
        // else pressedCode = primaryCode;


        //  added next sections for repeating backspace
        primaryCode = getCharFromKey(pressedCode, LatinKeyboardView.direction,mInputView.getKeyboard());
        if (primaryCode == Keyboard.KEYCODE_DELETE && !repeating)
            handleBackspace();
        if (primaryCode == KEYCODE_DELETEWORD && !repeating)
            deleteLastWord();
        repeating = false;

        if (RepeatBSTimer != null) {
            RepeatBSTimer.cancel();
            RepeatBSTimer = null;
        }
        //  moved all the rest of this method from onKey()
        int[] keyCodes;

        //  added this var for Andro 2+ multitouch
        keyCodes = keyCodesSave;

        commitTyped(getCurrentInputConnection());


        if (isWordSeparator(primaryCode) && (char) primaryCode != '.'

                && (char) primaryCode != '!' && (char) primaryCode != '?') {
            // Handle separator
            if (mComposing.length() > 0) {
                commitTyped(getCurrentInputConnection());
            }
            sendKey(primaryCode);
            updateShiftKeyState(getCurrentInputEditorInfo());
        } else if (primaryCode == Keyboard.KEYCODE_DELETE) {
            //  commented out next line for repeating backspace
            // handleBackspace();
        } else if (primaryCode == Keyboard.KEYCODE_SHIFT || primaryCode == -1) {
            handleShift();
        } else if (primaryCode == Keyboard.KEYCODE_CANCEL) {
            handleClose();
            return;
        } else if (primaryCode == KEYCODE_ESCAPE) {
            // Do nothing on Escape key
        } else if (primaryCode == LatinKeyboardView.KEYCODE_OPTIONS) {
            // Show a menu or something
        } else if (primaryCode == Keyboard.KEYCODE_MODE_CHANGE
                && mInputView != null) {
            Keyboard current = mInputView.getKeyboard();
            if (current == mSymbolsKeyboard
                    || current == mSymbolsShiftedKeyboard) {
                getCurrentInputConnection().finishComposingText();
                current = mQwertyKeyboard;
            } else {
                getCurrentInputConnection().finishComposingText();
                current = mSymbolsKeyboard;
            }
            mInputView.setKeyboard(current);
            if (current == mSymbolsKeyboard) {
                current.setShifted(false);
            }
        } else if (primaryCode == KEYCODE_CAPSLOCK)// handle caps lock
        {
            if (mInputView.getKeyboard() == mQwertyKeyboard
                    || mInputView.getKeyboard() == mSymbolsKeyboard) {
                mInputView.setKeyboard(mQwertyKeyboardUpperCase);
                mQwertyKeyboardUpperCase.setShifted(true);
                mCapsLock = true;
            } else {
                mQwertyKeyboard.setShifted(false);
                mInputView.setKeyboard(mQwertyKeyboard);
                mCapsLock = false;
            }
        } else if (primaryCode == KEYCODE_DELETEWORD) {
            //  commented out next line for repeating backspace
            // deleteLastWord();
        } else if (primaryCode == KEYCODE_FULL_STOP_AND_SPACE) {
            //  added next line
            backspaceIfSpaceLeft();
            getCurrentInputConnection().finishComposingText();

            handleCharacter((int) '.', keyCodes);
            handleCharacter((int) ' ', keyCodes);
            handleShift();

        }
        //  added next 5 KEYCODES
        else if (primaryCode == KEYCODE_EXCLAMATION) {
            //  added next line
            backspaceIfSpaceLeft();
            getCurrentInputConnection().finishComposingText();

            handleCharacter((int) '!', keyCodes);
            handleCharacter((int) ' ', keyCodes);
            handleShift();

        } else if (primaryCode == KEYCODE_QUESTION_MARK) {
            //  added next line
            backspaceIfSpaceLeft();
            getCurrentInputConnection().finishComposingText();

            handleCharacter((int) '?', keyCodes);
            handleCharacter((int) ' ', keyCodes);
            handleShift();

        } else if (primaryCode == KEYCODE_COMMA) {
            //  added next line
            backspaceIfSpaceLeft();
            getCurrentInputConnection().finishComposingText();

            handleCharacter((int) ',', keyCodes);
            handleCharacter((int) ' ', keyCodes);

        } else if (primaryCode == KEYCODE_COLON) {
            //  added next line
            backspaceIfSpaceLeft();
            getCurrentInputConnection().finishComposingText();

            handleCharacter((int) ':', keyCodes);
            handleCharacter((int) ' ', keyCodes);

        } else if (primaryCode == KEYCODE_SEMICOLON) {
            //  added next line
            backspaceIfSpaceLeft();
            getCurrentInputConnection().finishComposingText();

            handleCharacter((int) ';', keyCodes);
            handleCharacter((int) ' ', keyCodes);

        } else {
            handleCharacter(primaryCode, keyCodes);
        }
}

谢谢..

最佳答案

这是一个非常长的 onTouchEvent 处理程序,我建议将其分解为更符合逻辑的步骤。在尝试处理触摸屏时,我也遇到了看似“乱序”事件的问题。

我发现我没有正确处理每个指针 ID 的事件。我会检查以确保您按预期处理多个指针。我测试的设备 (N1) 仅支持两个指针,但其他设备支持更多指针,应该考虑到这些。

为了将触摸屏“软按钮”作为 onTouchEvent 事件处理,我发现创建状态机类很有用。使用 MotionEvent 参数作为状态机的输入事件,并导致状态转换以触发您想要的事件。一种明确的、状态驱动的方法将为您提供您正在寻找的预期结果。

关于android - 安卓软键盘,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9666177/

有关android - 安卓软键盘的更多相关文章

  1. ruby - 在 Ruby 中用键盘诅咒数组浏览 - 2

    我正在尝试在Ruby中制作一个cli应用程序,它接受一个给定的数组,然后将其显示为一个列表,我可以使用箭头键浏览它。我觉得我已经在Ruby中看到一个库已经这样做了,但我记不起它的名字了。我正在尝试对soundcloud2000中的代码进行逆向工程做类似的事情,但他的代码与SoundcloudAPI的使用紧密耦合。我知道cursesgem,我正在考虑更抽象的东西。广告有没有人见过可以做到这一点的库或一些概念证明的Ruby代码可以做到这一点? 最佳答案 我不知道这是否是您正在寻找的,但也许您可以使用我的想法。由于我没有关于您要完成的工作

  2. 安卓apk修改(Android反编译apk) - 2

    最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路

  3. 从零开始编写Web自动化测试脚本(六)--鼠标、键盘操作 - 2

    第一章Selenium+WebDriver环境搭建第二章Selenium定位方式第三章元素常用属性第四章自动化中的三种等待第五章自动化浏览器设置及句柄、窗口切换操作第六章鼠标、键盘操作第七章javascript在自动化中的应用第八章unittest&断言第九章ddt数据驱动第十章测试框架搭建过程Python+Selenium+BeautifulReport文章目录一、鼠标操作二、键盘操作一、鼠标操作1、在web测试中,鼠标的操作包含在ActionChains类中,经常用到的有单击、双击、右击、拖动等操作。2、在使用鼠标操作前需要先导入ActionChains类包:fromselenium.we

  4. Android Studio开发之使用内容组件Content获取通讯信息讲解及实战(附源码 包括添加手机联系人和发短信) - 2

    运行有问题或需要源码请点赞关注收藏后评论区留言一、利用ContentResolver读写联系人在实际开发中,普通App很少会开放数据接口给其他应用访问。内容组件能够派上用场的情况往往是App想要访问系统应用的通讯数据,比如查看联系人,短信,通话记录等等,以及对这些通讯数据及逆行增删改查。首先要给AndroidMaifest.xml中添加响应的权限配置 下面是往手机通讯录添加联系人信息的例子效果如下分成三个步骤先查出联系人的基本信息,然后查询联系人号码,再查询联系人邮箱代码 ContactAddActivity类packagecom.example.chapter07;importandroid

  5. Android 10.0 设置默认launcher后安装另外launcher后默认Launcher失效的功能修复 - 2

    1.前言 在10.0的系统rom定制化开发中,在系统中有多个launcher的时候,会在开机进入launcher的时候弹窗launcher列表,让用户选择进入哪个launcher,这样显得特别的不方便所以产品开发中,要求用RoleManager的相关api来设置默认Launcher,但是在设置完默认Launcher以后,在安装一款Launcher的时候,默认Launcher就会失效,在系统设置的默认应用中Launcher选项就为空,点击home键的时候会弹出默认Launcher列表,让选择进入哪个默认Launcher.所以需要从安装Launcher的流程来分析相关的设置。来解决问题设置默认La

  6. ruby - 如何使用 Ruby 将键盘和鼠标命令发送到底层操作系统? - 2

    是否有操作系统中立的方式让Ruby将键盘和鼠标事件发送到底层操作系统?(对我而言)一个明显的方法是使用Ruby/Java绑定(bind)并使用java.awt.Robot,但这看起来很愚蠢。 最佳答案 对于Mac:geminstallrb-appscript然后你可以用这样的脚本来测试它:require"rubygems"require"appscript"includeAppscriptapp("TextEdit").activateapp("SystemEvents").keystroke("LookMa,keystrokes!

  7. ruby - 使用 Selenium 和 Ruby 将键盘快捷键发送到 chrome - 2

    我正在尝试使用键盘快捷键在Linux上的chrome浏览器中启动开发工具。因为我使用的是Ruby并且它没有和弦方法,所以我尝试了以下方法:driver.action.key_down(:shift).key_down(:control).send_keys("i").key_up(:shift).key_up(:control).perform以上代码将在Firefox中运行(如Keypressin(Ctrl+A)SeleniumWebDriver中所建议),但在chrome中,它返回nil但没有结果。有什么建议吗? 最佳答案 在我

  8. AiBote 2022 新研发的自动化框架,支持 Android 和 Windows 系统。速度非常快 - 2

    Ai-Bot基于流行的Node.js和JavaScript语言的一款新自动化框架,支持Windows和Android自动化。1、Windowsxpath元素定位算法支持支持Windows应用、.NET、WPF、Qt、Java和Electron客户端程序和ie、edgechrome浏览器2、Android支持原生APP和H5界面,元素定位速度是appium十倍,无线远程自动化操作多台安卓设备3、基于opencv图色算法,支持找图和多点找色,1080*2340全分辨率找图50MS以内4、内置免费OCR人工智能技术,无限制获取图片文字和找字功能。5、框架协议开源,除官方node.jsSDK外,用户可

  9. Android Gradle 7.1+新版本依赖变化 - 2

    前一段时间由于工作需要把可爱的小雪狐舍弃了,找到了小蜜蜂。但是新版本的小蜜蜂出现了很多和旧版本不一样的位置。1.功能位置迁移,原来在工程build.gradle的buildscript和allprojects移动至setting.gradle并改名为pluginManagement和dependencyResolutionManagement。里面的东西依旧可以按照原来的copy过来。pluginManagement{repositories{gradlePluginPortal()google()mavenCentral()}}dependencyResolutionManagement{r

  10. ruby - Ruboto 的最佳教程(适用于 Android 的 ruby​​)? - 2

    关闭。这个问题不符合StackOverflowguidelines.它目前不接受答案。要求我们推荐或查找工具、库或最喜欢的场外资源的问题对于StackOverflow来说是偏离主题的,因为它们往往会吸引自以为是的答案和垃圾邮件。相反,describetheproblem以及迄今为止为解决该问题所做的工作。关闭9年前。Improvethisquestion我几乎用完了Ruby,但现在想试试Ruboto,android上的ruby​​。谷歌未能给我足够的(几乎没有结果)。所以任何人都可以分享一些关于Ruboto的教程。

随机推荐