Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello โ I'm @olemoritz on Twitter.
UIPageControl (Paging scrollview with page indicators)
-
size.width / 2 --> size.width // 2 to deal with size.width being odd?
-
@ccc, screen coordinates are floats.
-
@mikael Thank you again for this feedback.
What do you mean by:
- Exposing a
page_changed
callback with adelegate
parameter forself
.
I have not played around with callbacks very much and would appreciate some guidance on how to implement something like this.
- Exposing a
-
@Samer, I guess it sounds grander than it is.
What I meant is that in the
scrollview_did_scroll
method, you could check ifself
has an attributedelegate
, and if that object has a callable attribute calledpage_changed
, then we call that with some suitable parameters. Following the other Pythonista UI classes, parameters could beself
andpage_number
.scrollview_did_scroll
gets called a lot, not just once per page, so we would need to keep track of the previous page number and only call the delegate when there is a change.End result, people can set a delegate on your component to get notified when the page changes.
-
@mikael are you talking about somthing like this?
class MyDel: def __init__(self, sv): self.sv=sv self.current_page=sv.current_page self.prev_page=None self._refresh() def page_changed(self, scroll_view, page): pass def _refresh(self): if self.sv: if self.current_page != self.sv.current_page: self.page_changed(self.sv, self.sv.current_page) self.prev_page=self.current_page self.current_page=self.sv.current_page self.sv.refresh() class MyScroll(ui.View): def __init__(self): self.delegate=MyDel(self) self.current_Page=0 def _refresh(self): if self.delegate: self.delegate.refresh()
-
@stephen, not quite. The delegate (or the developer using the page control component) should not need to think about whether the page has changed; it only gets a notification when the page has changed.
Thus no
_refresh
, no book-keeping in the user's side, just aself.previous_page
to compare to in thescrollview_did_scroll
method of @Samer's component. -
@mikael ok i didnt click we are using an already exsisting callback method. i was thinking of a Base Class form implemention.
so like this?
class MyDel: def __init__(self, sv): self.current_page=sv.current_page self.prev_page=None def page_changed(self, sv, new-page): pass def scrollview_did_scroll(self, scrollview): if self.sv == scrollview: if self.current_page != scrollview.current_page: self.page_changed(scrollview, scrollview.current_page) self.prev_page=self.current_page self.current_page=self.sv.current_page self.sv.refresh() class MyScroll(ui.View): def __init__(self): self.delegate=MyDel(self) self.current_Page=0
but what happends if
self.current_page
changes with no call to delegatescrollview_did_scroll
due to button activation or key entry? wouldnt that delay any needed action on a method, for example, calledpage_opend
orpage_should_open
?i dont mean to over think i just want to make sure i understand completely ๐
EDIT:
wnted to clerify i do understand you mean use self as delegate and not other object i just separated for clearity. ๐
-
I just edited the post with v0.4. Let me know what you people think!
-
@Samer Good work brother ๐ is this for exploration of
ui
module or project? -
@stephen A bit of both, I have a project in the works that started out as a way to explore the ui module, it then morphed into learning about the objc_utils module a bit (MapView, ProgressView, UIPageControl) and then finally into wanting to create an app for the app store. This is just one part of a larger project. However I have probably had the most fun on this one part then all the others combined.
-
@Samer, good job!
Just a few more things come to mind. Seems like this fun is coming to an end.
- Recommend adding an explicit call to
self.layout()
at the end ofadd_subview
. Layout does get called without it, but only later, triggered by the UI loop, which can cause flickering when a view is momentarily visible in the wrong place. _trigger_delegate
is called both inset_page
andscrollview_did_scroll
. Sinceset_page
sets thecontent_offset
which then triggersscrollview_did_scroll
, you should not need to call_trigger_delegate
inset_page
at all.- Also, it would make more sense to me to call
_trigger_delegate
last inscrollview_did_scroll
, after the new page number has been set. The way it is now works only, I think, because there is so many scrolling events in every move from page to page. - Recommend moving
sampleDelegate
(should be upper case) andexample
within theif __name__
block. That way they do not unnecessarily get imported in "prudction use", and pollute the component's namespace.
- Recommend adding an explicit call to
-
@mikael And with that all the changes are implemented.
-
@Samer, tried it out, just to get the โdeveloper experienceโ.
image_names = ['test:Boat', 'test:Lenna', 'test:Mandrill', 'test:Peppers'] pages = PageControl( background_color='black', indicator_tint_color='grey', indicator_current_color='white' ) for image_name in image_names: pages.add_subview(ui.ImageView( image=ui.Image(image_name), content_mode=ui.CONTENT_SCALE_ASPECT_FIT)) pages.present('fullscreen', hide_title_bar=True )
Nice and tight, with nothing extra.
But, noticed a problem where on the first scroll the images jump down a little bit. I also tried changing the content size to the actual vertical content size, but then ended up having vertical scrolling, and images below the vertical center.
@Samer, which device are you using, are you seeing the same?
-
This post is deleted! -
This post is deleted! -
For some reason
self.scrollView.y
is 20pt too high and it fails to flexH
i setframe
andflex
and issue was corrected. settingy
to 19 still drops butbit is hard to see.self.scrollView = ui.ScrollView( delegate=self, paging_enabled=True, shows_horizontal_scroll_indicator=False, bounces=False, frame=(0, 20, w, h), flex='h',)
-
@mikael huh, thats very strange I get the same effect using your example on my IPad Pro 11โ (2018). It seems like initially the view overlaps with the info bar at the top of the screen (time, wifi, battery, etc) and upon touching the screen the view shifts down to not overlap with it. Itโs hard to tell in your example due to the black text on black background. However if you set
hide_title_bar
to true in the built in example a similar effect happens. I will look into a fix in the next day or two. -
@mikael @Samer ill Give a more indepth respone ๐ค
this is complete Script with comments and just commenting out changes instead if removing.
import ui from objc_util import ObjCClass, CGRect, create_objc_class, ObjCInstance, UIColor UIPageControl = ObjCClass('UIPageControl') def changePage(_self, _cmd): self = ObjCInstance(_self) self.page_control.set_page(self.page_control.pageControl.currentPage()) ChangePageClass = create_objc_class("ChangePageClass", methods=[changePage]) class PageControl(ui.View): def __init__(self, **kwargs): self.scrollView = ui.ScrollView( delegate=self, paging_enabled=True, shows_horizontal_scroll_indicator=False, bounces=False, frame=self.bounds, flex='WH', ) self.pageControl = UIPageControl.alloc().init().autorelease() self._target = ChangePageClass.new().autorelease() self._target.page_control = self self.pageControl.addTarget_action_forControlEvents_(self._target, 'changePage', 1 << 12) #1<<12 = 4096 self.pageControl.numberOfPages = len(self.scrollView.subviews) self.pageControl.currentPage = 0 self.pageControl.hidesForSinglePage = True self._prev_page = 0 super().add_subview(self.scrollView) ObjCInstance(self).addSubview_(self.pageControl) super().__init__(**kwargs) def layout(self): self.scrollView.content_size = (self.scrollView.width * len(self.scrollView.subviews), 0) safe_bottom = self.bounds.max_y - self.objc_instance.safeAreaInsets().bottom size = self.pageControl.sizeForNumberOfPages_(self.pageControl.numberOfPages()) self.pageControl.frame = CGRect( (self.bounds.center().x - self.bounds.width / 2, safe_bottom - size.height), (self.bounds.width, size.height)) for v in self.scrollView.subviews: v.width = self.bounds.width v.height = self.bounds.height v.x = v.pNum * self.bounds.width self.set_page(self.pageControl.currentPage()) def scrollview_did_scroll(self, scrollView): pageNumber = round(self.scrollView.content_offset[0] / (self.scrollView.content_size.width/len(self.scrollView.subviews)+1)) self.pageControl.currentPage = pageNumber self._trigger_delegate() def add_subview(self, v): v.pNum = len(self.scrollView.subviews) self.pageControl.numberOfPages = len(self.scrollView.subviews) + 1 self.scrollView.add_subview(v) self.layout() def _objc_color(self, color): return UIColor.colorWithRed_green_blue_alpha_(*ui.parse_color(color)) def _py_color(self, objc_color): return tuple( [c.floatValue() for c in objc_color.arrayFromRGBAComponents()]) if objc_color else None def _trigger_delegate(self): if hasattr(self, 'delegate'): if hasattr(self.delegate, 'page_changed'): if self.pageControl.currentPage() is not self._prev_page: self.delegate.page_changed(self.pageControl.currentPage()) self._prev_page = self.pageControl.currentPage() def set_page(self, page_number): if page_number < self.pageControl.numberOfPages() and page_number > -1: x = page_number * self.scrollView.width self.scrollView.content_offset = (x, 0) else: raise ValueError("Invalid Page Number. page_number is zero indexing.") @property def page_count(self): return self.pageControl.numberOfPages() @property def current_page(self): return self.pageControl.currentPage() @property def hide_on_single_page(self): return self.pageControl.hidesForSinglePage() @hide_on_single_page.setter def hide_on_single_page(self, val): self.pageControl.hidesForSinglePage = val @property def indicator_tint_color(self): """Returns un-selected tint color, returns None as default due to .pageIndicatorTintColor() returning that""" return self._py_color(self.pageControl.pageIndicatorTintColor()) @indicator_tint_color.setter def indicator_tint_color(self, val): self.pageControl.pageIndicatorTintColor = self._objc_color(val) @property def indicator_current_color(self): """Returns selected tint color, returns None as default due to .currentPageIndicatorTintColor() returning that""" return self._py_color(self.pageControl.currentPageIndicatorTintColor()) @indicator_current_color.setter def indicator_current_color(self, val): self.pageControl.currentPageIndicatorTintColor = self._objc_color(val) def update(self): print('!') def present(self, **kwargs): # evaluate input before presenting if kwargs['hide_title_bar']: self.scrollView.y=20 #if hide_title_bar then adjust scrollView super(PageControl, self).present(**kwargs) # Continue.. if __name__ == '__main__': # class SampleDelegate(): # def __init__(self): # pass # def page_changed(self, page_number): # """Gets called every time the page changes.""" # print(f'Delegate: {page_number}') # def btn_action(sender): # m.set_page(2) # m = PageControl() # sv = ui.View() # sv.background_color = 'red' # sv1B = ui.Button() # sv1B.title = "Go To Page 3 (index 2)" # sv1B.frame = (100,100,200,100) # sv1B.action = btn_action # sv.add_subview(sv1B) # sv2 = ui.View() # sv2.background_color = 'blue' # sv3 = ui.View() # sv3.background_color = 'yellow' # m.indicator_tint_color = 'brown' # m.indicator_current_color = 'black' # m.add_subview(sv) # m.add_subview(sv2) # m.add_subview(sv3) # m.delegate = SampleDelegate() # print(m.indicator_tint_color) # print(m.indicator_current_color) # print(m.page_count) # print(m.current_page) # print(m.hide_on_single_page) # m.present('fullscreen image_names = ['test:Boat', 'test:Lenna', 'test:Mandrill', 'test:Peppers'] pages = PageControl( background_color='black', indicator_tint_color='grey', indicator_current_color='white') for image_name in image_names: pages.add_subview(ui.ImageView( image=ui.Image(image_name), content_mode=ui.CONTENT_SCALE_ASPECT_FIT)) pages.present(style='fullscreen', hide_title_bar=False )
as you can see, just by lowering the
scrollView
20pt (i believe tjis is the title bar height), we fix the issue. by placing an override in ourPageControl
class we check ifhide_title_bar
and only ifTrue
we then lower they
attribute.I tested and even after changing
y
the childview's image can still completely cover the screen. this tells myself that somthing behind the scenes is not right. the image should not be able to display above 20 since the parent view is set to 20 fory
.@omz i also noticed that there is an issue if you stack too deep (3 deep in my case) in the UI Builder. App doesnt seem to hold onto the objects data and they disapear when you leave subviews for the given custom View object.
Main View - sub View - - sub sub View - - - datalost...
maybe in this case we might be feling a bit of this with the
ImageView
we are using?Obj-c instance - PageControl - - ScollView - - - ImageView
EDIT: what i mean is the imageview might be losing its placement data from creation so it defaults to having a tittle bar, and then when we initiate the event loop we remind it of its placement when delegate for scrolling gets fired..
And ofcourse this is with me asuming two things..
- objc instance and python are separate intities
- if so then objc takes persistance over the python view.
hopfully this helps get an idea of whats going on and hopfully im not wrong on these. this is just my perspective and im by no means an expert.
-
@stephen, probably related to the 20 pt at the top, yes. I would really like to resolve it in a more generic way than putting in a specific check for the
hide_title_bar
.Couple of data points regarding your other observations:
- ImageViews, by design or accident, do not have their clipsToBounds set. This is why the images do not seem to respect the 20 pt boundary.
- I have built many UIs with nesting deeper than 3 layers, and would say probably that is not the cause here.
I suspect there is something around the handling of the safe area that has changed around iOS 11 and has not been really addressed by Pythonista since.
What do you get on iPad if you check the safeAreaInsets for a fullscreen view?
-
@stephen, yes, this seems to be a bug or a feature, but not in @Samerโs component.
As a workaround, we can include an additional background view:
background = ui.View(background_color='black')
And then set the PageControl size only after the background has been presented:
background.present('fullscreen', hide_title_bar=True ) pages.frame = background.bounds background.add_subview(pages)
No jumping around then.