Você está em uma situação muito difícil, devo dizer.
Observe que você precisa usar um UIScrollView com pagingEnabled=YES
para alternar entre as páginas, mas precisa pagingEnabled=NO
rolar verticalmente.
Existem 2 estratégias possíveis. Não sei qual vai funcionar / é mais fácil de implementar, então tente os dois.
Primeiro: UIScrollViews aninhados. Francamente, ainda estou para ver uma pessoa que faça isso funcionar. No entanto, eu não tentei o suficiente pessoalmente, e minha prática mostra que quando você se esforça o suficiente, você pode fazer o UIScrollView fazer o que quiser .
Portanto, a estratégia é permitir que a exibição de rolagem externa trate apenas a rolagem horizontal e as exibições de rolagem interna apenas a rolagem vertical. Para fazer isso, você deve saber como o UIScrollView funciona internamente. Ele substitui o hitTest
método e sempre retorna a si mesmo, de forma que todos os eventos de toque vão para UIScrollView. Em seguida touchesBegan
, dentro , touchesMoved
etc , ele verifica se ele está interessado no evento e trata ou passa para os componentes internos.
Para decidir se o toque deve ser tratado ou encaminhado, o UIScrollView inicia um cronômetro quando você o toca pela primeira vez:
Se você não moveu seu dedo significativamente em 150 ms, ele passa o evento para a visualização interna.
Se você moveu seu dedo significativamente em 150 ms, ele começa a rolar (e nunca passa o evento para a visualização interna).
Observe como quando você toca em uma tabela (que é uma subclasse da visualização de rolagem) e começa a rolar imediatamente, a linha que você tocou nunca é destacada.
Se você não moveu seu dedo significativamente em 150 ms e o UIScrollView começou a passar os eventos para a visualização interna, mas então você moveu o dedo o suficiente para a rolagem começar, o UIScrollView chama touchesCancelled
na visualização interna e começa a rolagem.
Observe como, quando você toca em uma mesa, mantém o dedo um pouco pressionado e começa a rolar, a linha que você tocou é destacada primeiro, mas desmarcada depois.
Essa sequência de eventos pode ser alterada pela configuração do UIScrollView:
- Se
delaysContentTouches
for NÃO, então nenhum cronômetro é usado - os eventos vão imediatamente para o controle interno (mas são cancelados se você mover o dedo longe o suficiente)
- Se
cancelsTouches
for NÃO, uma vez que os eventos forem enviados para um controle, a rolagem nunca acontecerá.
Note que é UIScrollView que recebe tudo touchesBegin
, touchesMoved
, touchesEnded
e touchesCanceled
eventos de CocoaTouch (porque a suahitTest
diz-lhe para fazê-lo). Em seguida, ele os encaminha para a visão interna se quiser, pelo tempo que quiser.
Agora que você sabe tudo sobre o UIScrollView, pode alterar seu comportamento. Posso apostar que você deseja dar preferência à rolagem vertical, de modo que, assim que o usuário tocar na visualização e começar a mover o dedo (mesmo que ligeiramente), a visualização comece a se deslocar na direção vertical; mas quando o usuário move o dedo na direção horizontal longe o suficiente, você deseja cancelar a rolagem vertical e iniciar a rolagem horizontal.
Você deseja criar uma subclasse de seu UIScrollView externo (digamos, você nomeia sua classe como RemorsefulScrollView), de modo que, em vez do comportamento padrão, ele encaminhe imediatamente todos os eventos para a visualização interna e somente quando um movimento horizontal significativo for detectado ele role.
Como fazer RemorsefulScrollView se comportar dessa maneira?
Parece que está desativando a rolagem vertical e a configuração delaysContentTouches
como NÃO deve fazer com que UIScrollViews aninhados funcionem. Infelizmente, não; UIScrollView parece fazer alguma filtragem adicional para movimentos rápidos (que não podem ser desabilitados), de forma que mesmo se UIScrollView só puder ser rolado horizontalmente, ele sempre consumirá (e ignorará) movimentos verticais rápidos o suficiente.
O efeito é tão grave que a rolagem vertical dentro de uma visualização de rolagem aninhada é inutilizável. (Parece que você tem exatamente esta configuração, então tente: segure um dedo por 150 ms e, em seguida, mova-o na direção vertical - o UIScrollView aninhado funciona conforme o esperado então!)
Isso significa que você não pode usar o código do UIScrollView para manipulação de eventos; você tem que substituir todos os quatro métodos de manipulação de toque em RemorsefulScrollView e fazer seu próprio processamento primeiro, apenas encaminhando o evento para super
(UIScrollView) se você decidiu usar a rolagem horizontal.
No entanto, você deve passar touchesBegan
para UIScrollView, porque você deseja que ele se lembre de uma coordenada de base para rolagem horizontal futura (se mais tarde você decidir que é uma rolagem horizontal). Você não poderá enviar touchesBegan
para UIScrollView mais tarde, porque você não pode armazenar o touches
argumento: ele contém objetos que sofrerão mutação antes do próximo touchesMoved
evento e você não pode reproduzir o estado antigo.
Portanto, você deve passar touchesBegan
para o UIScrollView imediatamente, mas ocultará quaisquer touchesMoved
eventos posteriores até decidir rolar horizontalmente. Não touchesMoved
significa que não há rolagem, então esta inicial touchesBegan
não fará mal. Mas defina delaysContentTouches
como NÃO, de modo que nenhum temporizador surpresa adicional interfira.
(Offtopic - ao contrário de você, UIScrollView pode armazenar toques corretamente e pode reproduzir e encaminhar o touchesBegan
evento original posteriormente. Ele tem uma vantagem injusta de usar APIs não publicadas, então pode clonar objetos de toque antes que eles sofram mutação.)
Como você sempre encaminha touchesBegan
, você também deve encaminhar touchesCancelled
e touchesEnded
. Você tem que virar touchesEnded
em touchesCancelled
, no entanto, porque UIScrollView iria interpretar touchesBegan
, touchesEnded
seqüência como um toque de cliques, e iria enviá-lo para o ponto de vista interno. Você já está encaminhando os eventos apropriados, então nunca deseja que o UIScrollView encaminhe nada.
Basicamente, aqui está um pseudocódigo para o que você precisa fazer. Para simplificar, nunca permito a rolagem horizontal após a ocorrência do evento multitoque.
// RemorsefulScrollView.h
@interface RemorsefulScrollView : UIScrollView {
CGPoint _originalPoint;
BOOL _isHorizontalScroll, _isMultitouch;
UIView *_currentChild;
}
@end
// RemorsefulScrollView.m
// the numbers from an example in Apple docs, may need to tune them
#define kThresholdX 12.0f
#define kThresholdY 4.0f
@implementation RemorsefulScrollView
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.delaysContentTouches = NO;
}
return self;
}
- (id)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
self.delaysContentTouches = NO;
}
return self;
}
- (UIView *)honestHitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *result = nil;
for (UIView *child in self.subviews)
if ([child pointInside:point withEvent:event])
if ((result = [child hitTest:point withEvent:event]) != nil)
break;
return result;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event]; // always forward touchesBegan -- there's no way to forward it later
if (_isHorizontalScroll)
return; // UIScrollView is in charge now
if ([touches count] == [[event touchesForView:self] count]) { // initial touch
_originalPoint = [[touches anyObject] locationInView:self];
_currentChild = [self honestHitTest:_originalPoint withEvent:event];
_isMultitouch = NO;
}
_isMultitouch |= ([[event touchesForView:self] count] > 1);
[_currentChild touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if (!_isHorizontalScroll && !_isMultitouch) {
CGPoint point = [[touches anyObject] locationInView:self];
if (fabsf(_originalPoint.x - point.x) > kThresholdX && fabsf(_originalPoint.y - point.y) < kThresholdY) {
_isHorizontalScroll = YES;
[_currentChild touchesCancelled:[event touchesForView:self] withEvent:event]
}
}
if (_isHorizontalScroll)
[super touchesMoved:touches withEvent:event]; // UIScrollView only kicks in on horizontal scroll
else
[_currentChild touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if (_isHorizontalScroll)
[super touchesEnded:touches withEvent:event];
else {
[super touchesCancelled:touches withEvent:event];
[_currentChild touchesEnded:touches withEvent:event];
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesCancelled:touches withEvent:event];
if (!_isHorizontalScroll)
[_currentChild touchesCancelled:touches withEvent:event];
}
@end
Eu não tentei executar ou mesmo compilar isso (e digitei a classe inteira em um editor de texto simples), mas você pode começar com o descrito acima e, com sorte, fazê-lo funcionar.
O único problema oculto que vejo é que, se você adicionar visualizações filho não UIScrollView ao RemorsefulScrollView, os eventos de toque que você encaminha para um filho podem chegar de volta a você via cadeia de resposta, se o filho nem sempre lida com toques como o UIScrollView faz. Uma implementação RemorsefulScrollView à prova de balas protegeria contra touchesXxx
reentrada.
Segunda estratégia: se por algum motivo UIScrollViews aninhados não funcionarem ou se mostrarem muito difíceis de acertar, você pode tentar se dar bem com apenas um UIScrollView, trocando sua pagingEnabled
propriedade em tempo real de seu scrollViewDidScroll
método delegado.
Para evitar a rolagem diagonal, você deve primeiro tentar lembrar o contentOffset in scrollViewWillBeginDragging
e verificar e redefinir o contentOffset interno scrollViewDidScroll
se detectar um movimento diagonal. Outra estratégia a ser tentada é redefinir contentSize para permitir a rolagem apenas em uma direção, depois de decidir para qual direção o dedo do usuário está indo. (UIScrollView parece bastante tolerante quanto a mexer com contentSize e contentOffset em seus métodos delegados.)
Se isso também não funcionar ou resultar em visuais desleixados, você deve substituir touchesBegan
, touchesMoved
etc. , e não encaminhar eventos de movimento diagonal para UIScrollView. (A experiência do usuário será subótima neste caso, no entanto, porque você terá que ignorar os movimentos diagonais em vez de forçá-los em uma única direção. Se você estiver se sentindo realmente aventureiro, pode escrever seu próprio UITouch parecido com algo como RevengeTouch. Objetivo -C é o C simples e antigo, e não há nada mais complicado no mundo do que C; contanto que ninguém verifique a classe real dos objetos, o que eu acredito que ninguém faz, você pode fazer qualquer classe se parecer com qualquer outra. Isso abre a possibilidade de sintetizar qualquer toque que você quiser, com as coordenadas que você quiser.)
Estratégia de backup: há TTScrollView, uma reimplementação sã do UIScrollView na biblioteca Three20 . Infelizmente, parece muito pouco natural e não-iphonish para o usuário. Mas se todas as tentativas de usar UIScrollView falharem, você pode voltar para uma visualização de rolagem com código personalizado. Eu não recomendo, se possível; o uso do UIScrollView garante que você obtenha a aparência e o comportamento nativos, não importa como ele evolua nas futuras versões do iPhone OS.
Ok, este pequeno ensaio ficou um pouco longo demais. Ainda estou interessado nos jogos UIScrollView depois de trabalhar no ScrollingMadness há vários dias.
PS: Se você conseguir algum desses funcionando e quiser compartilhar, envie-me um e-mail com o código relevante para andreyvit@gmail.com. Eu o incluiria em meu pacote de truques do ScrollingMadness .
PPS Adicionando este pequeno ensaio ao README de ScrollingMadness.