如何让监听器在手机屏幕上上下滑动时仍然工作?

4
我正在开发一个小型库,用于进行心理实验。您可以在这里 此处 进行实时尝试,也可以在下面的片段中尝试代码。

var selfConcept = (function() {
  //privates    
  var activeListenerStepNames = [];
  var accuracyFeedbackDuration = 400;
  var blankInterval = 1500;
  var clickListenerHandler = function(e) {
    buttonTouched = '';

    switch (e.target.id) {
      case 'd-button':
        buttonTouched = 'D';
        break;
      case 'k-button':
        buttonTouched = 'K';
        break;
    }

    e.data = buttonTouched;

    window.performance.mark(markName);
    window.performance.measure(measureId, generateMarkName('start'), markName);

    responseTimes.push({
      'stepId': currentStep,
      'stimulus': steps[currentStep]['stimulus'],
      'responseTime': window.performance.getEntriesByName(measureId)[0]["duration"],
      'key': e.data
    });

    //fix for Android
    document.getElementById("hidden-input").value = '';

    drawSetting();
    nextStep();
  };
  var currentStep = 0;
  var fixationCrossDuration = 1000;
  var frameId;
  var keyListenerHandler = function(e) {

    if (String.fromCharCode(e.keyCode) == 'D' || String.fromCharCode(e.keyCode) == 'K') {
      window.performance.mark(markName);
      window.performance.measure(measureId, generateMarkName('start'), markName);

      responseTimes.push({
        'stepId': currentStep,
        'stimulus': steps[currentStep]['stimulus'],
        'responseTime': window.performance.getEntriesByName(measureId)[0]["duration"],
        'key': String.fromCharCode(e.keyCode),
      });

      isAccuracyFeedbackDisplayed = false;
      removeListener(window, 'input', markName, keyListenerHandler);

      nextStep();
    }
  };
  var spaceListenerHandler = function(e) {
    if (String.fromCharCode(e.keyCode) == ' ') {
      e.preventDefault();
      removeListener(document, "keydown", markName, spaceListenerHandler);
      drawSetting();
      nextStep();
    }
  }
  var isAccuracyFeedbackDisplayed = false;
  var measureId;
  var responseTimes = [];
  var steps = [];
  var groupInstruction;

  function addListener(element, event, name, eventFunction) {
    element.addEventListener(event, eventFunction);
    activeListenerStepNames.push(name);

    //console.log(activeListenerStepNames);
  }

  function drawSetting(text, color, background) {
    text = (text === undefined) ? '' : text;
    color = (color === undefined) ? 'black' : color;
    background = (background === undefined) ? 'white' : background;

    workAreaDiv = document.getElementById("work-area");
    workAreaDiv.innerHTML = "";

    div = document.createElement('div');

    div.id = 'stimulus';
    div.style.color = color;
    div.innerHTML = text;
    div.style.fontSize = '280%';

    div.style.class = 'col';

    workAreaDiv.appendChild(div);
  }

  function fixForMobilePhones() {
    $('#work-area-container').removeClass('h-100').addClass('h-75');
    $('#container').append('<div class="row h-25"><div id="d-button" style="background-color: black; color: white; border-right: 1px solid white;" class="col-6 text-center"><h1>NO</h1></div><div id="k-button" style="background-color: black; color: white;" class="col-6 text-center"><h1>YES</h1></div></div>');

    addListener(document.getElementById("d-button"), 'click', markName, clickListenerHandler);
    addListener(document.getElementById("k-button"), 'click', markName, clickListenerHandler);
  }

  function generateMarkName(name) {
    return name + '-' + steps[currentStep]["type"] + '-' + currentStep;
  }

  function isMobile() {
    if (navigator.userAgent.match(/Android/i) ||
      navigator.userAgent.match(/webOS/i) ||
      navigator.userAgent.match(/iPhone/i) ||
      navigator.userAgent.match(/iPad/i) ||
      navigator.userAgent.match(/iPod/i) ||
      navigator.userAgent.match(/BlackBerry/i) ||
      navigator.userAgent.match(/Windows Phone/i)
    ) {
      return true;
    }

    return false;
  }

  function nextStep() {
    var nextStep = currentStep + 1;

    if (nextStep in steps) {
      currentStep = nextStep;
      markName = generateMarkName('start');
      window.performance.mark(markName);
      //console.log("mark - markName:"+markName);
    } else {
      window.cancelAnimationFrame(frameId);
      frameId = undefined;

      alert('end');
    }
  }

  function randomizeSteps() {
    words = ["word1", "word2", "word3", "word4", "word5", "word6", "word7", "word8", "word9", "word10"];
    trials = [];

    index = 0;
    for (index = 0; index < words.length; index++) {
      trials.push({
        'id': index,
        'type': 'trial',
        'stimulus': words[index]
      });
    }

    instructions = [

      {
        'type': 'duration',
        'stimulus': '',
        'duration': blankInterval
      },
      {
        'type': 'duration',
        'stimulus': '+',
        'color': 'black',
        'duration': fixationCrossDuration
      },
      {
        'type': 'duration',
        'stimulus': '',
        'duration': blankInterval
      }
    ];

    trials = shuffleArray(trials);

    trialsWithBlankInterval = [];

    for (itemIndex = 1; itemIndex < trials.length; itemIndex++) {
      trialsWithBlankInterval.push(trials[itemIndex]);
      trialsWithBlankInterval.push({
        'type': 'duration',
        'stimulus': '',
        'duration': fixationCrossDuration
      });
    }

    steps = instructions.concat(trialsWithBlankInterval);
  }

  function removeListener(element, event, name, eventFunction) {
    element.removeEventListener(event, eventFunction);
    activeListenerStepNames.splice(activeListenerStepNames.indexOf(name), 1);
  }

  /* function copied from https://dev59.com/IXE95IYBdhLWcg3wHqMV */
  function shuffleArray(array) {
    for (let i = array.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [array[i], array[j]] = [array[j], array[i]];
    }

    return array;
  }

  function startTimer() {
    frameId = requestAnimationFrame(startTimer);
    window.performance.mark('frame');
    measureId = 'measure-' + currentStep;

    // console.log("TYPE:"+steps[currentStep]["type"]);

    if (isMobile() == true && $('#work-area-container').hasClass('h-100')) {
      fixForMobilePhones();
    }

    switch (steps[currentStep]["type"]) {
      case 'instructions':
        document.getElementById("work-area").classList.remove('text-center');
        document.getElementById("work-area").classList.add('text-justify');

        document.getElementById("work-area").innerHTML = steps[currentStep]["html"];

        if (activeListenerStepNames.indexOf(markName) == -1) {
          addListener(document, 'keydown', markName, spaceListenerHandler);
        }

        break;

      case 'duration':
        document.getElementById("work-area").classList.remove('text-justify');
        document.getElementById("work-area").classList.add('text-center');
        drawSetting(steps[currentStep]["stimulus"]);

        window.performance.measure(measureId, generateMarkName('start'), 'frame');
        performanceEntries = window.performance.getEntriesByName(measureId);

        var max = 0;
        for (var i = 0; i < performanceEntries.length; i++) {
          if (parseInt(performanceEntries[i]["duration"]) > max)
            max = performanceEntries[i]["duration"];
        }

        if (max >= steps[currentStep]["duration"]) {
          //console.log('step: ' + currentStep);
          //console.log(performanceEntries[performanceEntries.length - 1]["duration"]);
          nextStep();
        }

        break;

      case 'trial':
        document.getElementById("work-area").classList.remove('text-justify');
        document.getElementById("work-area").classList.add('text-center');
        drawSetting(steps[currentStep]["stimulus"], steps[currentStep]["color"], steps[currentStep]["background"]);

        markName = generateMarkName('response');

        if (activeListenerStepNames.indexOf(markName) == -1) {
          //console.log(activeListenerStepNames);
          addListener(document, 'keydown', markName, keyListenerHandler);
        }

        break;
    }
  }

  //public
  return {
    init: function() {
      randomizeSteps();

      markName = generateMarkName('start');
      window.performance.mark(markName);
      console.log("INIT");
      startTimer();
    }
  }
})();

selfConcept.init();
html,
body {
  height: 100%;
  font-size: 100%;
}

.container {
  height: 100%;
}

input.transparent {
  opacity: 0;
  filter: alpha(opacity=0);
}

.text-overflow {
  overflow: auto;
  -webkit-overflow-scrolling: touch;
}

.likert .row>.col,
.likert .row>[class^="col-"] {
  padding-top: .75rem;
  padding-bottom: .75rem;
  background-color: rgba(86, 61, 124, .15);
  border: 1px solid rgba(86, 61, 124, .2);
}
<div id="container" class="container">

  <div id="work-area-container" class="row h-100 text-overflow">
    <div class="col my-auto">
      <div id="work-area" class="w-100 mx-auto text-justify"></div>
      <input id="hidden-input" class="transparent" type="text" readonly="readonly">
    </div>
  </div>

</div>

用户需要根据两个或更多选项(在此示例中为“NO”和“YES”)对出现在屏幕中央的刺激进行分类。我需要测量从刺激出现到用户键入键盘(或在移动电话上轻敲)的延迟时间(以毫秒为单位)。
为了实现这个目标,我使用了Web APIs,特别是Performance对象。此外,我使用了Bootstrap4使其具有响应性。在桌面或笔记本电脑上的浏览器中一切正常:用户可以使用D键(表示“NO”)和K键(表示“YES”)来回答刺激。
我请求您的帮助的问题仅发生在移动电话上,其中答案模式基于两个可见按钮:“NO”和“YES”。我注意到当我无意中在屏幕上向上或向下滑动时(尤其是多次这样做),监听器不再起作用。就像窗口失去了焦点,因此我必须两次轻敲按钮才能使它们起作用(我想第一次重新获得焦点,第二次触发事件),破坏了延迟时间的测量。
我尝试以以下方式解决这个问题,但它不起作用:
document.addEventListener('touchstart', this.touchstart);
document.addEventListener('touchmove', this.touchmove);

function touchstart(e) {
    e.preventDefault();
}

function touchmove(e) {
    e.preventDefault();
}

编辑: 我也尝试了以下修复方法,灵感来源于TheMindVirus的建议

var lastScrollPosition = 0;
window.onscroll = function(event)
{
    if((document.body.scrollTop >= 0) && (lastScrollPosition < 0))
    {
        // first try
        window.focus();

        // second try after having assigned tabindex='1' to the div "work-area-container"
       $('#work-area-container').focus();
    }
    
    lastScrollPosition = document.body.scrollTop;
}

我该如何解决这个问题?非常感谢。


请添加适用于台式机和笔记本电脑的代码。最好的方法是将其包装在可运行的堆栈片段中,以便每个人都可以在台式机和移动设备上测试它。 - biberman
已完成。但是代码片段在移动设备上也能正常工作。请在您的移动设备上访问以下链接以查看问题:http://178.62.83.15/test-so/test.html - Gianluca78
在代码片段和链接的测试网站中,只有一个单词出现,没有按钮。可以使用开发人员工具找到隐藏的输入。 - biberman
这是预期的行为。正如我在帖子中所写的,在台式机或笔记本电脑上,用户可以使用D键(表示“否”)和K键(表示“是”)来响应刺激。按钮仅出现在移动版本中。 - Gianluca78
1个回答

1

你在self-concept.js中的事件监听器和处理程序看起来很好,但是你代码片段中的两个是错误的。必须使用普通函数调用:

document.addEventListener('touchstart', touchstart);
document.addEventListener('touchmove', touchmove);

你可以在浏览器控制台中看到带有文件名和行号的错误:
test.html:51
[Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive. 

这意味着'this'在这里无法工作...

我用以下设备测试了您的测试页面:

Galaxy S4 (Android 5):

  • opera mini (可用)
  • firefox (可用)
  • chrome (白屏)
  • samsung browser (白屏)

Galaxy S7 (Android 7):

  • firefox (可用)
  • chrome (可用)
  • samsung browser (可用)

iPhone 4s (IOS 9.3.5)

  • safari (白屏)

我认为问题可能不是您的代码,而是您的手机或浏览器...


谢谢您的建议,我已经修复了错误,但问题仍然存在。如果我上下拉动屏幕(使窗口弹跳),监听器有时不再起作用,我必须两次点击按钮才能回答。 - Gianluca78
抱歉,我忘记在函数调用中传递事件('e')- 我已经在答案中修复了它。顺便说一下,我在4个移动浏览器中测试了您的网站。在其中两个浏览器中,它无论如何都可以正常工作(更改屏幕方向,滑动,缩放),而在另外两个浏览器中,我无论如何都只能看到一个白屏... - biberman
已经修正了拼写错误,但问题仍然存在。请问您能否列出它可以或无法工作的浏览器? - Gianluca78
没关系。感谢您的时间。不幸的是,当我从屏幕顶部(或底部)向下滑动两次或更多时,听众仍然无法工作。 - Gianluca78
已在Galaxy S4(Android 5)上测试:可用的浏览器有Opera Mini和Firefox;白屏的浏览器有Chrome和Samsung浏览器... - biberman
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接