반응형
블로그 이미지
개발자로서 현장에서 일하면서 새로 접하는 기술들이나 알게된 정보 등을 정리하기 위한 블로그입니다. 운 좋게 미국에서 큰 회사들의 프로젝트에서 컬설턴트로 일하고 있어서 새로운 기술들을 접할 기회가 많이 있습니다. 미국의 IT 프로젝트에서 사용되는 툴들에 대해 많은 분들과 정보를 공유하고 싶습니다.
솔웅

최근에 올라온 글

최근에 달린 댓글

최근에 받은 트랙백

글 보관함

카테고리


반응형
Other methods and properties


$.mobile.changePage () 이외에 jQuery Mobile은 추가적인 메소드와 프로퍼티들을 제공합니다. 이것도 $.mobile object에  set 됩니다.


Methods defined on the $.mobile object


Method / Property

Signification

showPageLoadingMsg ()

Method to display a waiting message indicating a page is loading. The message is defined in $.mobile.loadingMessage string ("loading" by default).

hidePageLoadingMsg ()

Method to hide the waiting message previously shown by showPageLoadingMsg ().

activePage

Property representing the jQuery class object for the window currently displayed on the screen.

firstPage

Property representing the jQuery class object corresponding to the first window described in the HTML page (which will normally be displayed first).

pageContainer

Property representing the jQuery class object corresponding to the element in which the windows are inserted. By default the parent element of the first window, corresponding generally to the <body> element. Note that jQuery Mobile assigns the ui-mobile-viewport CSS class to this element.
The value of this property can be changed only during processing of the $ (document).ready () event or after it. New displayed windows will then be inserted in this element.


현재 active window의 HTML content 를 알기 위해 $.mobile.activePage property 를 사용합니다. 우리는 alert 안에 이 content를 display 하는 window에 link를 insert 합니다.


View HTML content of the active window


<!DOCTYPE html>

<html> 

<head> 

  <meta name=viewport content="user-scalable=no,width=device-width" />

  <link rel=stylesheet href=jquery.mobile/jquery.mobile.css />

  <script src=jquery.js></script>

  <script src=jquery.mobile/jquery.mobile.js></script>

</head> 


<body> 


<div data-role=page id=home>

  <div data-role=header>

    <h1>Home</h1>

  </div>


  <div data-role=content>

    <p> Window content </p>  

    <a href=# id=link1> View the contents of the active window </a>

  </div>

</div>


</body>

</html>


<script>


$("#link1").bind ("click", function (event)

{

  alert ($.mobile.activePage.html ());

});




tistory412_01.html





반응형


반응형

Parallax Simplified

Posted on . Written by

I

이번주 화요일의 튜토리얼은 코로나 Ambassador 인 Brent Sorrentino 씨가 해 주셨습니다. parallax scrolling 과 여러분 앱에 customizable touch-and darg parallax view 를 구현하는 데모 프로젝트에 대해 다룹니다.


We Live in a Parallax World



Parallax 는 천문학, photography,광학 에서 다루는 개념인데요 좀 의미가 광범위 합니다.  모바일 게임 개발자의 경우 2-dimensional scrolling simulation 에 대해 작업할 경우 parallax는 카메라와 움직이는 객체 사이의 거리감 (sense of “distance”) 을 제공하게 되죠.  이 메소드는 16 비트 닌텐도 시절부터 사용되어져 왔습니다. 그리고 요즘 인기있는 Angry Birds 와 Squids 등에서도 사용되고 있죠.


코로나의 display groups를 활용하면 차례로 겹쳐진 layer 들로 parallax scrolling을 간단하게 구현할 수 있습니다. 이론적으로는 그런데 실제 많은 개발자들이 실제 이것을 코딩하려고 하면 많이 헛갈려 하시죠. 오늘의 튜토리얼과 데모 프로젝트는 이 부분에 촛점이 맞춰져 있습니다.




1. Getting Started


처음으로 우리가 할 작업은 우리의 parallax "layers" 들을 display groups 로 configure 하는 겁니다. main.lua 파일에 아래 코드를 복사해 넣으세요.


--First, declare a local reference variable for the stage.
local stage = display.currentStage

--Declare your parallax layers in back-to-front order and specify their distance ratio.
--This "distanceRatio" sets the layer's scrolling speed in relation to the foreground (1.0) layer.
local back00 = display.newGroup() ; back00.distanceRatio = 0.2
local back01 = display.newGroup() ; back01.distanceRatio = 0.4
local back02 = display.newGroup() ; back02.distanceRatio = 0.6
local back03 = display.newGroup() ; back03.distanceRatio = 0.8
local foreground = display.newGroup() ; foreground.distanceRatio = 1.0

--IMPORTANT NOTE 1: You MUST have one layer set to 1.0 ratio, even if it won't contain any objects.
--IMPORTANT NOTE 2: Attach a reference variable named "refGroup" to the stage table and set its
--value to the 1.0 layer from above. This group will be used as the core reference group in
--relation to screen touch-and-drag.
stage.refGroup = foreground


  

각 parallax layer 는 “distanceRatio” parameter를 정해 줍니다. 이것은 아주 중요한 부분입니다. 우리는 이것을 foreground 의 1:1 scrolling 과 연결해서 각 레이어별로 스크롤링 스피드를 주는부분에서 사용할 겁니다.  만약 특정 layer 가 parallax scrolling에서 제외되어야 한다면 간단하게 이 파라미터를 undeclare 하면 됩니다.  예를 들어 이 display group setup의 hierarchy 중에 한개 나 두개의 layer 를 scroll 하고 싶지 않다면 , 예를 들어 UI/button/widget layer 같은 경우가 될 수 있겠죠, 이럴 경우 이 distanceRatio 값을 넣지 않으면 됩니다. 이 “distanceRatio” parameter 가 있는 layer 만 parallax scrolling 이 될 테니까요.


Additional note: “distanceRatio” parameter 는 여러분이 원하신다면 1.0 보다 클 수도 있습니다.  예를 들어 1.4로 했다면 이 레이어는 foreground 보다 더 빨리 움직일 겁니다.  이건 아마 카메라와 foreground 사이에 연한 안개가 있도록 할 경우에 활용할 수 있겠네요.


2. Setting Scroll Limits


대부분의 시나리오에서 어떤 제한된 공간(세계) 가 있게 되죠. 유저가 어느쪽으로 어디까지 drag 할 수 있는지에 대한 제한 등이 그런 것들인데요. 이런것들은 다음에 나오는 4개의 변수들에 의해 해결할 수 있습니다. 다음에 이것들을 쉽게 사용하도록 하기 위해 stage 의 파라미터들로서 이 4개를 정의해 둡니다.

여러분의 프로젝트에 다음 한 줄을 추가해 주세요.


stage.xMin = -400 ; stage.xMax = 400 ; stage.yMin = -150 ; stage.yMax = 1000
 

이 4개의 파라미터가 간단하게 그 제한된 공간을 나타낼 겁니다. xMin는  대개 음수가 되겠죠. 왼쪽까지 얼마나 멀리 그 공간이 정해질지를 정합니다. xMax는 오른쪽까지의 공간이구요. yMinyMax는 위쪽과 아래쪽으로 스크롤할 수 있는 공간을 나타낼겁니다.

이 숫자들은 모두 픽셀단위를 사용합니다. % 가 아닙니다. 또한 foreground 1:1 레이어는 움직이지 않을 거구요. 이 foreground layer 이외의 layer 들은 이 제한사항들이 적용될 겁니다.


3. Add Layers to a Containing Table


이제 우리는 이 parallax layer 들을 paraGroups라는 이름의 테이블에 포함시킬 겁니다. 그리고 그 테이블은 stage 의 sub-table 이 될 거구요.

아래 코드를 여러분 프로젝트에 복사해 넣으세요.


stage.paraGroups = {}
for g=1,stage.numChildren do
  if ( stage[g].distanceRatio ) then stage.paraGroups[#stage.paraGroups+1] = stage[g] end
end

  

이 코드를 보면 첫번째로 테이블을 정의합니다. 그리고 display groups 에 대해 stage 의 각 child 숫자 만큼 루프를 돌립니다. 그리고 if 문을 사용해서 “distanceRatio” parameter가 있는 경우에만 테이블에 집어 넣습니다.


4. Populate the World


다음 코드에서는 gradient background를 세팅할 겁니다. 그리고 our world 에 몇개의 샘플 객체들을 추가할 거구요. 이 코드들에 대해서는 일일이 설명하지는 않을께요. 여러분들은 여러분들 나름대로의 parallax world 를 만들실 거니까요. 아래에서 하나의 객체를 각 parallax layer 에 add 하는 것을 보실텐데요. 여러분은 많은 객체들과 이미지들을 추가하셔야 한다는 것만 기억해 두세요.


local cX = display.contentWidth/2 ; local cY = display.contentHeight/2
local bkgrad = graphics.newGradient( {42,17,10}, {0,0,0}, "up" )
local bk = display.newRect( 0,0,cX*2,cY*2 ) ; bk:setFillColor( bkgrad ) ; bk:toBack()

local object00 = display.newRect( back00,cX-16,cY-16,32,32 ) ;
    object00:setFillColor(150,0,50,120)
local object01 = display.newRect( back01,cX-32,cY-32,64,64 ) ;
    object01
:setFillColor(150,20,50,140)
local object02 = display.newRect( back02,cX-48,cY-48,96,96 ) ;
    object02
:setFillColor(150,40,50,160)
local object03 = display.newRect( back03,cX-64,cY-64,128,128 ) ;
    object03
:setFillColor(150,60,50,180)
local foreObject = display.newRect( foreground,cX-80,cY-80,160,160 ) ;
    foreObject
:setFillColor(150,80,50,200)


5. Implementing Touch and Drag

여러분이 움직일 수 없다면 이 scrollable world 는 소용이 없겠죠. 아래 함수는 screen touch 의 "began" 과 "moved" phases 를 사용해서 paraGroups table 안에 있는 모든 그룹들을 스크린 내에서 여러분이 drag 할 수 있도록 만들겁니다.

이 함수와 touch listener 를 여러분 프로젝트에 복사해 넣으세요.


local function touchScreen( event )
  local stageID = event.target
  local refGroup = stageID.refGroup ; local paraGroups = stageID.paraGroups
  local eventX = event.x ; local eventY = event.y

  if ( #paraGroups < 1 ) then return end

  if ( event.phase == "began" ) then

    for i=1,#paraGroups do
      paraGroups[i].offsetX = eventX - refGroup.x
      paraGroups[i].offsetY = eventY - refGroup.y
    end

  elseif ( event.phase == "moved" ) then

    local xDif = eventX - refGroup.offsetX
    local yDif = eventY - refGroup.offsetY

--If you are NOT limiting the boundaries of your world, comment out these 2 lines.
    if ( xDif < stageID.xMin ) then xDif = stageID.xMin
    elseif
( xDif > stageID.xMax ) then xDif = stageID.xMax end
    if ( yDif < stageID.yMin ) then yDif = stageID.yMin
    elseif ( yDif > stageID.yMax ) then yDif = stageID.yMax end

    for i=1,#paraGroups do
      local group = paraGroups[i]
      group.x = xDif * group.distanceRatio
      group.y = yDif * group.distanceRatio
      group = nil
    end
    xDif, yDif = nil,nil

  end

  stageID, refGroup, paraGroups, eventX, eventY = nil,nil,nil,nil,nil
  return true

end

stage:addEventListener( "touch", touchScreen )



이 코드에 대해서도 일일이 설명을 드리지는 않겠습니다. 단지 이 behavior를 summarize 할께요.


began phase에서 우리는 paraGroups table에 대해 루프를 돌립니다. 그리고 the foreground reference group 에 대해 각각 특정 X/Y 값을 세팅합니다. 이것은 core 1:1 group 의 포지션에 근거해서 각 layer들의 위치를 정해주기 위해 필요한 부분 입니다.


moved phase 는 실제 action 이 일어나는 곳입니다. 우선 첫번째로 어디에 유저가 touch 했는가에 따라 관련 group 들을 움직이기 위해 xDifyDif ”delta” values를 정해야 합니다. 그리고 바로 직전의 phase check 으로부터 얼마나 멀리 움직였는지도 정해야 하구요.


그 다음으로는 위의 Step #2 에서 정해 놓은 world boundary 들의 바깥으로 X, Y 포지션이 나갔는지 여부를 체크합니다. 4개 중에 하나라도 밖으로 나가게 되면 xDifyDif value 를 pre-adjust  합니다. 그렇게 함으로서 그 world 밖으로는 절대 나가는 일이 없게 되는거죠. 코드 안에 적어놨듯이 scrolling 의 제한을 주고 싶지 않으면 이 부분을 주석을 달 수도 있습니다. 


마지막으로는 paraGroups table에 루프를 돌리고 각각의 parallax layer의 위치를 조정합니다. 이 때 group 에서 정해 줬던 distanceRatio value에 따라 위치를 조정하게 됩니다. distance ratio 가 1.0 인 foreground group은 정확히 유저의 touch 를 따라서 움직이게 됩니다. 그리고 그 이외에 layer 들은 여러분이 정해 준 distance ratio 에 따라서 약간씩 다르게 움직입니다. 한번 ratio들을 바꾸고 나서 다시 시도해 보세요. 움직임이 다를 겁니다. 여러번 테스트 해 보시고 가장 그럴듯한 ratio 들을 정하셔서 사용하세요.



아주 간단하죠? 이제 몇개의 layer 든지 상관없이 scrolling touch-and-drag parallax world 를 구현할 수 있는 flexible 한 메소드를 구현했습니다. 이제 실전에서는 각각의 ratio 들만 조정해서 사용하시면 됩니다. 아니면 database 나 text file을 이용해서 dynamically 움직임들을 조정 할 수도 있죠.


6. Cleaning Up


마지막으로 layer references들을 clean up 하는 것을 잊지 마세요. 특히 여러분이 Storyboard 나 Director 와 같은 scene manager 를 사용하고 있다면 더 잊지 마셔야 됩니다. 각 scene manager 에서 사용하는 standard cleanup practices 를 사용하는 것 외에 parallax display groups 에 대한 references 를 가지고 있는 paraGroups table 을 clean up 하는 것도 중요합니다. scene 들이 바뀔 때마다 이 작업들이 이루어 지도록 해야 합니다. touch listener도 remove 하는 것 잊지 마시구요. (전체 메소드가 전체 scene 에서 작동하는 global level 이면 필요가 없겠지만요.)



for i=#stage.paraGroups,1,-1 do
  stage.paraGroups[i] = nil
end
stage:removeEventListener( "touch", touchScreen )


Looking Forward!


다음 튜토리얼에서는 이 메소드를 build 하고 dynamic 하게 parallax layer 를 zoom in/out 하는 간편한 zoom feature에 대해 다루겠습니다. 그 때까지 이 데모 project 를 구현하고 수정하고 테스트 해 보시고 질문이나 제안이 있으면 댓글 달아 주세요.



반응형

Dialog windows 사용하기

2012. 10. 17. 08:28 | Posted by 솔웅


반응형
Dialog windows


Display a dialog window


$.mobile.changePage () method는 overlapping windows를 display 하기 위해서 사용 될 수도 있습니다. 이 overlay 윈도우는 그 작업을 위해 data-role="dialog" attribute 를 가지고 있어야 합니다.


Use the $.mobile.changePage () to display a layered window


<!DOCTYPE html>

<html> 

<head> 

  <meta name=viewport content="user-scalable=no,width=device-width" />

  <link rel=stylesheet href=jquery.mobile/jquery.mobile.css />

  <script src=jquery.js></script>

  <script src=jquery.mobile/jquery.mobile.js></script>

</head> 


<body> 


<div data-role=page id=home>

  <div data-role=header>

    <h1>Home</h1>

  </div>


  <div data-role=content>

    <p> Window content 1 </p>  

    <a href=# id=link1> Goto window 2 </a>

  </div>

</div>


<div data-role=dialog id=win2>

  <div data-role=header>

    <h1>Window 2</h1>

  </div>


  <div data-role=content>

    <p> Window content 2 </p>  

  </div>

</div>

</body>

</html>


<script>


$("#link1").bind ("click", function (event)

{

  $.mobile.changePage ($("#win2"), { transition : "pop" });

});


</script>






tistory410_01.html


Close a dialog window


dialog 윈도우를 닫는것은 대개 jQuery Mobile에 의해 추가된 close button 을 클릭해서 닫게 됩니다. 그 외에도 윈도우의 content 안에 버튼을 넣어서 이 closing 메카니즘을 만들어서 사용해도 상관없습니다. 그러려면 button 을 처리하는 어떤 과정이 필요합니다. 윈도우에 연관된 <div>에서 dialog ("close") method를 call 하는 부분을 코딩해야 합니다.


Use a close button in the dialog window


<!DOCTYPE html>

<html> 

<head> 

  <meta name=viewport content="user-scalable=no,width=device-width" />

  <link rel=stylesheet href=jquery.mobile/jquery.mobile.css />

  <script src=jquery.js></script>

  <script src=jquery.mobile/jquery.mobile.js></script>

</head> 


<body> 


<div data-role=page id=home>

  <div data-role=header>

    <h1>Home</h1>

  </div>


  <div data-role=content>

    <p> Window content 1 </p>  

    <a href=# id=link1> Goto window 2 </a>

  </div>

</div>


<div data-role=dialog id=win2>

  <div data-role=header>

    <h1>Window 2</h1>

  </div>


  <div data-role=content>

    <p> Window content 2 </p>  

    <a id=close data-role=button> Close window </a>

  </div>

</div>

</body>

</html>


<script>


$("#link1").bind ("click", function (event)

{

  $.mobile.changePage ($("#win2"), { transition : "pop" });

});


$("#close").bind ("click", function (event)

{

  $("#win2").dialog ("close");

});


</script>






tistory410_02.html


Remove the close button in the dialog window


마지막으로 overlay window의 title bar 안에 jQuery Mobile 에 의해 add 된 close 버튼을 어떻게 remove 하는지에 대해 알아보겠습니다. 이것은 title bar에 있는 <a> element 와 관련이 있습니다. 그리고 selector $(this).find ("div: jqmData (role=header) a")를 통해서 접근될 수 있습니다. (만약 이것이 overlay window 라면요.)


우리는 이 element를 jQUery Mobile에 의해 overlay window 가 transforme 되기 위한 HTML 코드 이후에 접근할 수 있습니다. 이 때 dialogcreate event 와 견결 됩니다. 왜냐하면 overlay window 는 jQuery Mobile standard component dialog에 의해 handle 되기 때문입니다.


So we write:


Remove the close button in the dialog window


<!DOCTYPE html>

<html> 

<head> 

  <meta name=viewport content="user-scalable=no,width=device-width" />

  <link rel=stylesheet href=jquery.mobile/jquery.mobile.css />

  <script src=jquery.js></script>

  <script src=jquery.mobile/jquery.mobile.js></script>

</head> 


<body> 


<div data-role=page id=home>

  <div data-role=header>

    <h1>Home</h1>

  </div>


  <div data-role=content>

    <p> Window content 1 </p>  

    <a href=#win2 id=link1> Goto window 2 </a>

  </div>

</div>


<div data-role=dialog id=win2>

  <div data-role=header>

    <h1>Window 2</h1>

  </div>


  <div data-role=content>

    <p> Window content 2 </p>  

  </div>

</div>

</body>

</html>


<script>


$("#win2").bind ("dialogcreate", function ()

{

  $(this).find ("div:jqmData(role=header) a").hide ();

});


</script>



dialog window 도 (다른 윈도우들과 마찬가지로) pagecreate event를 받습니다.  하지만 이 이벤트는 dialogcreate event 이전에 받게 됩니다. jQuery Mobile 에 의해 HTML 이 transform 된것을 inform 하지 않는 거죠. 그래서 dialogcreate event 를 사용해야 되는 겁니다.




tistory410_03.html



이제 dialog 윈도우는 더 이상 close button 이 없습니다.




반응형

윈도우 생성 과정 이해하기

2012. 10. 16. 21:29 | Posted by 솔웅


반응형


Process of creating windows



jQuery Mobile내에서 윈도우 생성 과정을 이해하는 것은 아주 중요한 일입니다. 윈도우는 $.mobile.changePage ()$.mobile.loadPage () methods 가 calling 됐을 때 생성됩니다. 이 두 메소드는 모두 enhancePage () internal method를 call 합니다. 이 메소드는 해당 윈도우가 이미 생성된 윈도우가 아니라면 윈도우를 생성하는 작업을 진행합니다.


Processing of the pagecreate event


윈도우를 생성하기 위해 enhancePage () method 는 jQuery Mobile page component와 연관 돼 있는 page () method를 call 합니다. 이 page () method는 page component 를 생성하죠. 지난 번 글에서 봤듯이 컴포넌트의 메인 메소드를 call 하는 것은 이벤트를 발생시킵니다. "create"  과 연결된 컴포넌트 이름이 그 이름입니다. (여기서는  pagecreate event). jQuery Mobile 소스코드에서 보실 수 있듯이 pagecreate event는 코드의 다른 부분으로 다뤄집니다.


pagecreate event processing to create the window (in jquery.mobile.js)


$( ":jqmData(role='page'), :jqmData(role='dialog')" )

 .live( "pagecreate", function( e ) {

    var $page = $( this ),
        o = $page.data( "page" ).options,
        pageTheme = o.theme;

    $( ":jqmData(role='header'), 

        :jqmData(role='footer'), 

        :jqmData(role='content')", this ).each(function() {


        /* … */ 
       
        //apply theming and markup modifications to page,header,content,footer
        if ( role === "header" || role === "footer" ) {


            /* … */

 
        } else if ( role === "content" ) {


            /* … */

 
        }
    });

});

  

이 코드 블럭은 data-role="page"data-role="dialog" attributes들과 함께 있는 element를 jQuery Mobile windows 에서 style 로 바꿔 줍니다. title bar 가 header, footer, content 인 HTML element들이 CSS 클래스가 추가 됨으로서 다른 모습으로 바뀌는 것을 잘 봐 두실 필요가 있습니다.



jQuery Mobile source code를 다시 살펴 보면 pagecreate event 에 대해 논의 할 수 있는 다른 코드 부분을 보실 수 있을 겁니다. 예를 들어...


Creation of input fields having jQuery Mobile styled way (in jquery.mobile.js)


//auto self-init widgets
$( document ).bind( "pagecreate create", function( e ){

    $( $.mobile.textinput.prototype.options.initSelector, e.target )
        .not( ":jqmData(role='none'), :jqmData(role='nojs')" )
        .textinput();

});



이 코드 블럭은 document object가 이 이벤트를 listening 하고 있는 것을 보여 줍니다. 그것을 receive 하면 jQuery Mobile textinput () method 는  (input field들을 managing 하고 있는 textinput component 로부터) 모든 HTML elements 들을 matching 되는 selector ($.mobile.textinput.prototype.options.initSelector에서 정의 된)에 apply 합니다.  그리고 e.target element의 descendants 에 위치 됩니다. 이 메소드를 call 하면 이전 글에서 봤듯이 textInput component의 생성을 진행합니다.


이 element 의 descendants 내에 있는 HTML elements 들에 대해서만 search 하기 위해 e.target parameter를 사용하는 것에 대해 주목해 주세요. 그래야지만 좀 더 빨리 작업을 할 수 있겠죠. 마지막으로는 selector에 의해 정의된 input field들에 해당하는 모든 element들이 jQuery Mobile 스럽게 보이는 input fields 들로 transform 될 겁니다. 바로 document object 가 pagecreate event 를 받은 거죠.


이 윈도우에서의 컴포넌트 생성 프로세스는 jQuery Mobile component 와 연결된 모든 HTML elements 에 대해 implement 됩니다. 그 관계는 아래 표와 같습니다.



jQuery Mobile standard components


Name

Signification

page

Windows management in the page

checkboxradio

Checkboxes and radio buttons management

textinput

Input texts management

selectmenu

Select menus management

button

Buttons management

slider

Sliders management

collapsible

Accordion menus management

listview

Listviews management

dialog

Overlapped windows management


이 프로세스로 인해 우리는 radio buttons, check boxes, input fields, etc. 등을 style 할 수 있는거죠.


어플리케이션에서 display 된 첫번째 윈도우는 $.mobile.initializePage () method 가 실행되는 동안 jQuery Mobile에 의해 자동적으로 생성됩니다 .그리고 위에 설명드린 transformation 작읍을 할 $.mobile.changePage () method를 call 하는 것이죠.


Creation of standard components in jQuery Mobile


위 코드 블럭은 어떻게 jQuery Mobile standard component 가 윈도우를 생성할 때 pagecreate event 가 보내지면서 무슨 일을 하게 되는지를 잘 보여줍니다. 그 컴포넌트와 연관된 이 메소드(previously textinput ())는 slector 와 연결돼 있는 HTML element에서 call 됩니다. 그럼으로서 그 컴포넌트를 생성하게 되는겁니다.


또한 이 작업은 HTML elements들이 create event를 receive 할 때 진행되는 것도 보있습니다. bind () method의 파라미터에 그것이 표현돼 있죠. 이 의미는 jQuery Mobile standard components들을 두가지 방법으로 생성할 수 있다는 것을 말합니다.


  • Either by triggering a create event on the window containing the HTML element, which leads to the creation of the component via the processing performed in the received event;

  • Or by directly calling on the HTML element, the method for creating the component (eg method textinput () to create the input field).


이 두가지 방법들은 같은 동작을 하게 되구요 같은 결과를 보여 줍니다. 그 컴포넌트는 그것을 포함하고 있는 윈도우안에 생성되게 되는 것이죠. 이 글 다음에 이어지는 글들에서 이 jQuery Mobile 이 제공하는 standard components들을 다이나믹하게 생성하는 방법들에 대해 다룰 것입니다.


Synchronization of the creation of components in the window


pagecreate event는 윈도우가 생성되는 동안에 보내 집니다. 그리고 the document object가 그것을 받게 되죠. jQuery Mobile treatment는 윈도우 내에서 그 컴포넌트들을 각각 생성하기 위해 활용됩니다. 이 컴포넌트들을 생성하는 것은 이러한 컴포넌트들과 관련된 create type 의 새로운 이벤트를 발생시킵니다.

예를 들어


  • checkboxradiocreate for a checkboxradio component corresponding to a checkbox or radio button,

  • textinputcreate for a textinput component corresponding to an input field,

  • slidercreate for a slider component corresponding to a slider,

  • Etc..


그러므로 한 윈도우를 생성함으로서 윈도우와 관련된 여러 이벤트들이 발생되게 됩니다.
(pagecreate) 또한 컴포넌트들에는 (checkboxradiocreate, textinputcreate, etc.) 것들도 포함되는 거죠. 이러한 이벤트들은 각각 독립적입니다. 그러므로 pagecreate event의 도입부는 윈도우의 content 가 이미 생성되었다는 것을 의미하지는 않습니다. 정확하게는 그 윈도우의 컴포넌트가 생성되는 jQuery Mobile에 의해 이 이벤트의 프로세스가 진행되고 있다는 의미죠.



반응형


반응형

AppQ


What is AppQ?


AppQ는 Kurogo Mobile Framework 의 하나의 component 입니다. 이것은 모바일 웹 모듈을 iOS 와 Android native application으로 빠르게 porting 해 주는 기능을 하죠. AppQ는 hybrid 앱 시스템입니다. 네이티브 엡 안에 웹뷰로 embedded 된 Kurogo Mobile Web Module들이 있게 되는 것이죠.


AppQ Features


AppQ uses a native navigation stack


  • iOS (and to a lesser extent Android 4.0+) page transitions 들은 아주 독특합니다. 그리고 웹 기술로는 구현하기가 아주 어렵죠.  AppQ 모듈들은 좀 더 나은 user experience를 드리기 위해 다른 hybrid 솔루션들보다 더 native 에 가깝게 느끼도록 설계돼 있습니다.
  • page transition들이 natively 처리 되기 때문에 AppQ 모듈들은 jQuery Mobile이나 Sencha Touch 같은 라이브러리들을 필요로 하지 않습니다. 이러한 large 자바스크립트 라이브러리들은 페이지 load time을 증가시키고 hybrid 앱들의 퍼포먼스를 아주 낮추는 영향을 주게 됩니다.


AppQ modules can co-exist with native Kurogo-iOS and Kurogo Android modules


  • native와 AppQ 모듈들을 쉽게 mixing 하고 matching 하도록 함으로서 여러분들은 기능적인 면에 좀 더 집중할 수 있게 됩니다. 쿠로고 모듈들이 native 에 fully implementation 되도록 AppQ 가 도와드립니다.


AppQ modules use templates customized for each native platform


  • AppQ leverages the Kurogo Mobile Web’s template inheritance system to allow you to customize your UI elements for each native platform.
  • Stock Kurogo Mobile Web templates are already customized for the native platforms.


AppQ modules store CSS, Javascript and images inside the native app
(AppQ 모듈은 native 앱 안에 CSS, Javascript, 이미지등을 저장합니다.)


  • 이렇게 함으로서 페이지 로딩 시간과 네트워크 bandwidth use를 절감시킬 수 있습니다.
  • 여러분의 native app 에 이런 파일들을 포함시킴으로서 이 앱을 시작할 때 일반적인 모바일 웹 보다 좀 더 빠르게 로딩할 수 있도록 해 줍니다.
  • 여러분의 모바일 웹 UI를 업데이트 할 때 native 앱들은 모듈 파일들의 new version들을 다운로드 할 겁니다.


AppQ 1.0 Technical Capabilities and Limitations


어떤 Kurogo Mobile Web module 이라도 AppQ 모듈이 될 수 있습니다. 여러분이 웹 모듈로 만들 수 있다면 AppQ 모듈로도 만들 수 있습니다. 웹 모듈과 마찬가지로 AppQ 모듈들은 Kurogo Mobile Web server 로부터 live content 를 display 합니다. AppQ 는 아직까지 여러분의 native 앱이 offline viewing 을 위한 저장이나 cache 작업을 충분히 지원하지는 못합니다.


1.0 버전에서는 아래와 같은 기능과 제한이 있습니다.


What AppQ 1.0 can do:


  • Anything a Kurogo Mobile Web module can do, including:
    • Module development with HTML, CSS and Javascript technologies
    • Kurogo Mobile Web theming support

  • Ability to update module look and feel without releasing new versions of the native apps

  • Use custom “native look and feel” assets for common UI elements and built-in Kurogo mobile web templates

  • Access to native UI elements:
    • Navigation bar: can set title, back button title and add a refresh content button
    • Alert dialogs
    • Action dialogs (cancel, optional destructive button, and up to 10 other optional buttons)
    • Share dialog (automatically via Smarty share.tpl template)
    • Native mail composition dialog when user taps on a mailto: link

  • Basic GPS location as provided by HTML5 web engine (navigator.geolocation)

What AppQ 1.0 can’t do:

  • Offline storage
    • Providing AppQ offline storage similar to what is available on fully native apps is a difficult technical and architectural problem
    • HTML5 offline storage is too small for many use cases
    • Access to native storage mechanisms is a difficult technical and architectural problem on Android
      • On Android the APIs necessary for this (web view cache) were introduced in Ice Cream Sandwich (ICS)
        • Currently most devices are on Gingerbread so we cannot require ICS
      • All supported iOS versions can provide this functionality via NSURLCache and CoreData

  • Camera access

  • NFC access

  • Other direct sensor access


Preparing a Web Module for AppQ

대부분의 모듈들은 약간의 작업만 하면 AppQ를 적용할 수 있습니다. 약간의 modification은 필요하죠. 특히 많은 javascript 나 conditional UI display 가 있는 complex 모듈의 경우는 좀 더 작업이 필요할 겁니다.

Telling AppQ about your module pages


AppQ 가 알아야할 여러분의 모듈에 대한 것들 중 하나는 어떤 페이지들이 지원될 것이냐는 겁니다. AppQ는 이 정보를 모듈의 pages.ini configuration 파일에서 가져 옵니다. AppQ가 제대로 작동할 수 있도록 여러분 모듈의 각 페이지들이 pages.ini 에 리스트 되야 합니다.


pages.ini

[index] [detail] pageTitle = "Detail" [search] pageTitle = "Search Results" breadcrumbTitle = "Search"


Locating images and other static assets

AppQ는 웹 모듈에서 사용되는 자바스크립트, css , 이미지 등의 asset을 zip 파일로 빌드합니다.  이런 파일들이 검색되면 모든 템플릿 variable들과 함께 이 파일들은 template에 로드되게 됩니다. 만약 여러분 웹 모듈이 템플릿 variable이 set 됐을 떄 특정 UI element들만 보여준다면 AppQ는 그러한 element들을 찾지 못할 겁니다. 이것을 하기 위해 각 모듈을 initializeForPage() 의 custom version 을 정의할 수 있는데요. 이것은 AppQ에 의해 사용되는 initializeForNativeTemplatePage() 라고 불립니다. 이 기능을 정의할 때 탬플릿 variable들을 세팅하셔야 합니다. 그러면 이미지나 CSS, 자바스크립트 파일까지 포함한 모든 UI element 들이 visible 하게 됩니다.


예를 들어 여기 index와  search 페이지에 에 ellipsizer javascript module을 사용하는 모듈이 있습니다. 그리고 detail page에 share button 을 가지고 있구요.


MyWebModule.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
 <?php
 protected function initializeForNativeTemplatePage() {
     // Native template support
     // specify anything that goes into the header or footer here
     // and force the appearance of assets so they get loaded into the template
     switch ($this->page) {
         case 'index':
             // force appearance of section select button
             $this->assign('sections', array(1, 2));
         case 'search':
             $this->addInternalJavascript('/common/javascript/lib/ellipsizer.js');
             break;

         case 'story':
             $this->assign('shareTitle', $this->getLocalizedString('SHARE_THIS_STORY'));
             $this->assign('shareEmailURL', 'dummy');
             $this->assign('shareRemark',   'dummy');
             $this->assign('storyURL',      'dummy');
     }
 }


두번째 옵션은 array 에 있는 파일들을 정해주기 위한 겁니다. 이 옵션은 어떤 asset을 display 하기 위해 많은 code 가 요구 될 때 간단하게 작업할 수 있는 옵션입니다.


MyWebModule.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 <?php
 protected function nativeWebTemplateAssets() {
     return array(
         '/min/g=file:/common/javascript/lib/ellipsizer.js',
         '/common/images/share.png',
         '/common/images/button-email.png',
         '/common/images/button-facebook.png',
         '/common/images/button-twitter.png'
     );
 }


세번째 옵션은 module.ini 파일에 필요한 내용들 입니다. 이 옵션은 여러분의 custom module theme 에 이미지를 add 하고 subclass에서는 add 하고 싶지 않을 때 아주 유용하죠.


module.ini

[module]
title = "AppQ Test"
disabled = 0
protected = 0
search = 1
secure = 0
MAX_RESULTS = 10
SHARING_ENABLED = 1

[native_template]
additional_assets[] = "/min/g=file:/common/javascript/lib/ellipsizer.js"
additional_assets[] = "/common/images/share.png"
additional_assets[] = "/common/images/button-email.png"
additional_assets[] = "/common/images/button-facebook.png"
additional_assets[] = "/common/images/button-twitter.png"

[strings]
help[] = "The news home screen features most recent news across all categories. Click on an individual news item to read the full story. Note that clicking a link within the story will launch your browser. You can share each article using email, Facebook, or Twitter by clicking on the gray arrow button top right."


Javascript global variables


가끔 웹 모듈들은 글로벌 namespace로 variable들을 세팅합니다. 그러면 로딩된 자바스크립트 파일들에서 계속 참조되겠죠.


AppQ-incompatible global variable use


아래 예제를 보시죠. 이 site 는 custom header.tpl을 정의했습니다. 이 탬플릿에는 글로벌 자바스크립트 변수인 myGlobals 가 템플릿 변수의 배열로 정의돼 있구요.


header.tpl

{extends file="findExtends:common/templates/header.tpl"}
{block name="javascript"}
  <script type="text/javascript">
    var myGlobals = {json_encode($globalsArray)};
  </script>
  {$smarty.block.parent}
{/block}


그리고 이 site의 common.js 에서 myGlobals 변수가 참조 됩니다. (파일의 top level 에서)


common.js


var firstGlobal = myGlobals[0];



이런 테크닉은 AppQ 와 같이 사용할 수 없습니다. 왜냐하면 <head> tag 는 비어있고 자바스크립트 파일은 전에 generate 되고 페이지당 content 는 AJAX를 통해서 로드되기 때문이죠. 위와 같은 경우에는 myGlobals 값이 항상 null 일겁니다. 왜냐하면 globalsArray 가 html wrapper 가 generate 될 때 세팅되지 않을 것이기 때문입니다.


AppQ-compatible global variable use


AppQ 는 글로벌 변수의 사용을 모두 막지는 않습니다. key 는 built-in 모듈 함수인 WebModule::addInlineJavascript() WebModule::addInlineJavascriptFooter()를사용해 페이지마다 글로벌 자바스크립트 변수들을 정의하고 WebModule::addOnLoad() 사용해서 common.js 안의 function 을 trigger 해서 그것들을 reference 하도록 하는 겁니다. AppQ는 AJAX call 이 이루어진 이후에 이 자바스크립트 블럭을 있어야 할 위치에 자동으로 옮겨 놓습니다. 그리고 글로벌 namespace 에 선언하게 되죠.


예를 들어 위의 myGlobals variable 를 implementing 하는 AppQ-safe 방법은 header.tpl 를 오버라이딩하는 대신입니다. 우리는 extra 자바스크립트를 모듈의 php 파일에 옮깁니다.


MyWebModule.php


<?php
protected function initializeForPage() {
    parent::initializeForPage();
    $this->addInlineJavascript('var myGlobals = '.json_encode($this->globalsArray).';');
    $this->addOnLoad('myOnLoad();');
}



그리고나서 common.js 의 load 함수 내에 firstGlobal 을 initialize 하죠.


common.js


var firstGlobal = null;
function myOnLoad() {
    firstGlobal = myGlobals[0];
}



Using Native Callbacks


AppQ 는 간단한 작업으로 native 기능을 가능하도록 합니다.


Page Load


페이지 로드시 AppQ는 page title을 set 하고 그 페이지의  navigation stack 에 보여지게 될 back button 의 title을 세팅하게 됩니다. 디폴트로 AppQ는 모바일 웹에서 사용하는 page title과 breadcrumb title을 사용합니다. AppQ에서 다른 이름을 사용하고 싶으면 모듈의 pages.ini  의 nativePageTitlenativeBreadcrumbTitle 를 수정하시면 됩니다.



추가로 AppQ는 navigation bar에 reload button을 추가할 수 있도록 해 줍니다. 이 reload 버튼은 페이지의 content를 reload 할 수 있도록 해 주죠. 이것도 pages.ini  의 nativePageRefresh or programmatically 를 수정해 주시면 됩니다. 그리고 WebModule::setWebBridgePageRefresh() 함수도 사용하시구요.


Dialogs


hybrid 앱에서 유저에게 메세지를 안내하는 방법중이 하나가 dialogs 입니다. 이 dialog 는 top 에 URL을 가지고 있는 자바스크립트 팝업이던지 floating div 이고 native dialog 처럼 보이는 것이 될 겁니다. AppQ 모듈을 좀 더 native 처럼 만들도록 하기 위해 AppQ는 native dialog를 만들수 있는 자바스크립트 함수를 제공합니다.


Alert Dialogs


Alert dialog는 유저에게 기대하지 않은 상황이 초래 됐을 때 무엇인가를 알리기 위해 사용됩니다. 그리고 유저에게 cancel 이라던지 어떤 action을 할것인지를 물어보는 기능이 있을 겁니다.


kgoBridge.alertDialog(title, message, cancelButtonTitle, mainButtonTitle, alternateButtonTitle, statusCallback, buttonCallback)

  • title - (required) A short human readable title (shown in bold on the dialog)
  • message - (optional) A human-readable message (show in regular text below the title)
  • cancelButtonTitle - (required) Title of the button which dismisses the alert and cancels any actions the alert refers to
  • mainButtonTitle - (optional) Title of the primary button
  • alternateButtonTitle - (optional) Title of an alternate button
  • statusCallback - (optional) A callback function which will return an error if the dialog fails to display. The callback should have the following signature:
    • function statusCallback(error, params)
      • error - If there is no error, this will be null. If there is an error, the error object will contain the following properties:
        • code - a numeric code indicating what error occurred
        • title - a short string categorizing the error
        • message - a string describing the error
      • params - (ignored) always null
  • buttonCallback - (optional) A callback function which is called when one of the buttons is clicked. The callback should have the following signature:
    • function buttonCallback(error, params)
      • error - If there is no error, this will be null. If there is an error, the error object will contain the following properties:
      • code - a numeric code indicating what error occurred
      • title - a short string categorizing the error
      • message - a string describing the error
    • params - If there is no error, the params object will contain the following property:
      • button - with a string value indicating which button was tapped. This string may be one of:
        • cancel
        • main
        • alternate



일반적인 상황들을 간략화 하기 위해 아래 두가지 함수를 사용하실 수 있습니다.


kgoBridge.alert(message, responseCallback)


  • message - (required) A human-readable message
  • responseCallback - (optional) A callback function which will be called when the dialog is dismissed. The callback should have the following signature:
    • function responseCallback()


kgoBridge.confirm(question, responseCallback)


  • question - (required) A human-readable message
  • responseCallback - (optional) A callback function which will be called when the dialog is dismissed. The callback should have the following signature:
    • function responseCallback(confirmed)
      • confirmed - true if the user clicked “OK” and false if they clicked “Cancel”.


Action Dialogs


kgoBridge.actionDialog(title, cancelButtonTitle, destructiveButtonTitle, alternateButtonTitles, statusCallback, buttonCallback)


  • title - (required) A short human readable title
  • cancelButtonTitle - (required) Title of the button which dismisses the dialog and cancels the action the alert refers to
  • destructiveButtonTitle - (optional) Title of a destructive action if there is one (e.g. delete data). Button is emphasized or shown in red to warn user.
  • alternateButtonTitles - (required) An array of titles of additional buttons to display. Each button should correspond to a possible non-destructive action the user can take.
  • statusCallback - (optional) A callback function which will return an error if the dialog fails to display. The callback should have the following signature:
    • function statusCallback(error, params)
      • error - If there is no error, this will be null. If there is an error, the error object will contain the following properties:
        • code - a numeric code indicating what error occurred
        • title - a short string categorizing the error
        • message - a string describing the error
      • params - (ignored) always null
  • buttonCallback - (optional) A callback function which is called when one of the buttons is clicked. The callback should have the following signature:
    • function buttonClickedCallback(error, params)
      • error - If there is no error, this will be null. If there is an error, the error object will contain the following properties:
        • code - a numeric code indicating what error occurred
        • title - a short string categorizing the error
        • message - a string describing the error
      • params - If there is no error, the params object will contain the following property:
        • button - with a string value indicating which button was tapped. This string may be one of:
          • cancel
          • destructive
          • alternateN - where N is a number between 0 and the number of alternate buttons minus 1


일반적인 상황들을 간략화 하기 위해 아래 두가지 함수를 사용하실 수 있습니다.


kgoBridge.shareDialog(buttonConfig)


  • buttonConfig - (required) An object with the following properties
    • mail - (optional) a string containing a URL to share something via email (mailto:user@example.com)
    • facebook - (optional) a string containing a URL to share something on Facebook
    • twitter - (optional) a string containing a URL to share something on Twitter


일반적으로 kgobridge.shareDialog() function를 call 할 필요는 없습니다. 그냥 share.tpl 템플릿을 include 하시면 이 함수가 자동으로 call 할 겁니다.


Debug Logging


iOS 에서는 UIWebView 밖의 곳에서 logging message를 표시하는 console을 사용하기 어렵습니다. 이것을 하기 위해 AppQ는 logging function을 제공합니다. 이 함수는  native 앱이 디버그 모드에서 run 할 경우 NSLog()를 통해 Xcode console 로 메세지를 보낼 겁니다.  이 함수는 안드로이드에서도 사용할 수 있습니다.


kgoBridge.log(message)

  • message - (required) A human-readable message to log to the native console


Theming a Module for AppQ


Kurogo Mobile 웹은 stock 템플릿을 위해 native app images 와 styles를 활용합니다. 만약 여러분이 custom UI를 사용한다면 아마 AppQ 에 맞는 어떤 UI theme 을 사용하기를 우ㅝㄴ할 겁니다. 여러분은 모든 native platform에 적용하거나 아니면 특정 platform 에만 적용되는 AppQ 용 theme을 사용하실 수 있습니다. 이것은 모바일 웹에서 phone 브라우저의 서로 다른 type들을 구분하기 위해 사용한 pagetype/platform mechanism을 이용해서 구현할 수 있습니다.



Targeting AppQ devices


특별히 target native app을 하도록 도와주기 위해 AppQ는 세번째 classification type은 "browser" 를 제공합니다. 이것을 이용하면 각 pagetype과 platform 별로 그에 해당하는 브라우저도 구분할 수 있습니다.



예를 들어 아래 여러 CSS 가 있고 그 영향을 받을 device 들이 있습니다.


pagetype - platform - browser   Devices Impacted
common         .css all devices
compliant         .css all phones
tablet         .css all phones
common - android     .css all Android Devices
compliant - android     .css all Android phones
compliant - iphone     .css all iPhones
common - common - native .css AppQ on all devices
compliant - common - native .css AppQ on all phones
tablet - common - native .css AppQ on all tablets
common - android - native .css AppQ on Android devices
common - iphone - native .css AppQ on iOS devices
compliant - android - native .css AppQ on Android phones
compliant - iphone - native .css AppQ on iOS phones
tablet - android - native .css AppQ on Android tablets
tablet - iphone - native .css AppQ on iPad



자바스크립트 파일과 템플릿 그리고 이미지 directory들도 비슷한  naming scheme을 따릅니다.


Debugging AppQ theme problems


AppQ theming에서의 디버깅 문제는 full featured DOM inspector의 제한 때문에 device 에서 하기엔 어려운 점이 있습니다. 다행히도 AppQ 모듈을 디버깅하기 위해 Kurogo Mobile Web server 의 device debugging feature 를 사용할 수 있습니다.



우선 site.ini 파일에서 DEVICE_DEBUG=1이 되어 있어야 합니다. 그 다음 http://localhost/device/compliant-iphone-native/mymodule/로 가셔서 여러분 모듈의 AppQ iPhone module version을 보세요. 그리고 http://localhost/device/tablet-android-native/mymodule/은 모듈의 AppQ 안드로이드 태블릿 버전을 보여줄 겁니다. 이 때 native 앱의 브라우저와 유사한 웹 브라우저를 사용하셔야 될 겁니다. 예를 들어 iOS 와 안드로이드는 Webkit 브라우저를 사용하니까 Safari 나 Chrome 을 사용해서 디버깅 하셔야 됩니다.


AppQ는 native navigation stack 을 제공하기 때문에 AppQ device debugging mode 는 breadcrumb bar를 display 하지 않을 겁니다. 대신에 뒤로 가기 위해 browser navigation arrow를 사용하셔야 됩니다.


이 디버깅 모두는 AppQ에 의해 사용되는 AJAX 페이지 로딩을 simulate 합니다. 왜냐하면 지금 웹 브라우저에서 running 하고 있기 때문에 일반적으로 AppQ에서 가능한 native hooks 들을 implement 할 수 없기 때문이죠. 이들 중 하나를 trigger 하기위해 시도한다면 그 trigger URL을 console.log()  하려고 할 겁니다. 그러니까 어떤 파라미터드이 전달됐는지를 보려면 Web Inspector를 사용하시면 됩니다.


The AppQ Native Asset Zip File


일단 여러분의 모듈이 AppQ를 지원하도록 modify 되면 여러분의 첫번째 asset zip 파일을 빌드하실 수 있습니다. 이 archive 에는 모든 이미지와 CSS 그리고 자바스크립트가 포함 됩니다. 이것들은 퍼포먼스를 개선할 수 있도록 native 내에서 locally cache 되게 됩니다.



Building the Asset Zip File


asset zip 파일을 빌드하려면 Admin panel로 가셔서 왼쪽 메뉴의 Module Configuration을 선택하세요. 그러면 오른쪽에 모듈 이름 다음에 3개의 탭을 보실 수 있을 겁니다. 여기서 AppQ 탭을 선택하세요. iPhone 과 Android 템플릿으로 빌드하기 위한 버튼들을 보실 수 있을 겁니다. 빌드하고 싶은 것을 골라서 클릭하세요. AppQ asset zip 파일이 generate 될 겁니다. 파일 크기에 따라 시간이 좀 걸릴 수 있습니다. 


asset zip files이 모드 빌드 되면 각 버튼 밑에 이미지를 다운 로드 할 수 있는 링크를 보시게 될 겁니다. 이 링크는 다음과 같을 겁니다. http://www.example.com/media/web_bridge/iphone/mymodule.zip. 만약 이 사이트에 tablet theme을 enable 하도록 했다면 AppQ는 같은 asset zip file에 tablet version 도 같이 빌드 할 겁니다. http://www.example.com/media/web_bridge/iphone/mymodule-tablet.zip.


asset zip 파일들을 새롭게 빌드 할 때마다 Kurogo Mobile Web server는 새로운 파일을 다운로드 받으라고 알려 줄 겁니다. 아마 production server에 올릴 수 있도록 용량을 줄이기 위해 여러번 빌드 할 수 있을 겁니다. 그 때 마다 새로운 파일을 다운로드 받으셔야 합니다.


Preloading the Asset Zip File


AppQ asset zip 파일은 여러분의 site의 media 폴더 내의 Kurogo Mobile Web server 에 있습니다. Kurogo Mobile Web server는 native 앱에게 어떤 모듈이 AppQ를 지원하는지 얘기해 줍니다. 그리고 그 앱은 media 폴더에서 그 asset 파일들을 다운로드 받을 겁니다. 이 작업은 매번 asset zip file들이 change 될 때마다 반복될 겁니다. 이 asset zip 파일에 현재 버전을 넣는 다면 유저가 처음 앱을 시작할 때 훨씬 빨리 보여줄 수 있겠죠.


Admin panel은 이 asset zip file들의 copy를 얻을 수 있도록 다운로드 할 수 있는 link 를 제공합니다. 만약 여러분이 tablet 버전을 가지고 있다면 거기에 대한 링크도 있을 겁니다.


iOS (iPhone and iPad)


AppQ asset file은 iOS 에는 프로젝트의 resource 폴더에 있습니다. 아래에 iOS app “MyApp” 과 AppQ 모듈인 “mymodule” 에 대한 asset zip file path example 이 있습니다.

  • Kurogo-iOS/Projects/MyApp/Resources/modules/mymodule/mymodule.zip
  • Kurogo-iOS/Projects/MyApp/Resources/modules/mymodule/mymodule-tablet.zip


이 파일들은 Xcode 폴더 references를 통해서 여러분 프로젝트에 자동적으로 추가 될 겁니다.


Android


아래에 안드로이드 앱 “MyApp” 과  AppQ module “mymodule” 가 있는 경로 예제가 있습니다.

  • Kurogo-Android/site/MyApp/config/modules/mymodule/assets/web_bridge.zip
  • Kurogo-Android/site/MyApp/config/modules/mymodule/assets/web_bridge-tablet.zip


이 위치에 asset zip 파일들이 copy 되면 여러분 프로젝트를 clean 하시고 rebuild 하세요. 그러면 asset들이 여러분 앱에 built 될 겁니다.


Updating an AppQ Module


여러분의 모듈을 업데이트하고 그것을 Kurogo Mobile Web server 에 deploy 할 때마다 여러분은 이 새로운 asset zip file을 빌드할 필요가 있습니다. 그래야 그 zip 파일이 모바일 웹 버전과 일치할 수 있습니다. 현재 존재하는 native app은 new zip file을 다운로드 할 겁니다. 그러니까 여러분은 이미 이전 버전을 사용하고 있는 유저에 대해서는 걱정하실 필요가 없습니다. 일단 Kurogo Mobile Web server를 deploy 하고 난 후에 여러분은 새로운 zip 파일과 함께 여러분의 native 앱의 새 버전을 release 하고 싶어질 겁니다. 그래야 "first launch" experience 에서 새로운 버전의 asset zip file을 다운로드 받는 일이 없을 테니까요. 여러분의 모듈에 이미지가 아주 많이 있지 않다면 zip 파일은 그리 크지 않을 겁니다. 하지만 네트워크 속도가 느리다면 다운로드 시간이 noticeable 하게 될 겁니다.



반응형

'WEB_APP > Kurogo' 카테고리의 다른 글

Kurogo Tutorial 24 - People Module -  (0) 2012.10.10
Kurogo Tutorial 23 - Links Module -  (0) 2012.10.10
Kurogo Tutorial 22 - Content Module -  (0) 2012.10.10
Database Authentication  (0) 2012.09.12
Authentication  (0) 2012.09.06
Flat File Authentication  (0) 2012.09.06
Access Control and Authorization  (0) 2012.09.06
Kurogo Tutorial 21 - Emergency Module -  (0) 2012.07.12
Kurogo Tutorial 20 - Module Interaction -  (0) 2012.06.13
Kurogo Tutorial 19 - The Kurogo Object -  (0) 2012.06.12


반응형
$.mobile.loadPage (url, options) method



$.mobile.loadPage (url, options) method 는 $.mobile.changePage (toPage, options) method 에 의해 내부적으로(internally) 사용됩니다. 이것은 설정된 URL의 HTML 페이지를 수집(retrieves) 하고 메모리 내에 해당 윈도우를 load 합니다. 현재 보여지고 있는 윈도우는 바뀌지 않습니다. 그냥 다음 페이지를 내부적으로 다음 윈도우에 넣어서 메모리에 담아두고 있는거죠.


jQuery Mobile은 우리가 이 메소드를 사용하면 현재 display 되는 HTML 과 Load 되는 HTML 을 각각 분리할 수 있도록 처리해 줍니다. 이 메소드를 사용하면 예를 들어 pre-load 윈도우들의 경우 유저의 요구가 있었을 때 기다리는 시간 없이 즉시 표시해 줄 수 있도록 해 줍니다.  jQuery Mobile 은 data-prefetch attribute를 통해서 이런 작업을 합니다.


$.mobile.loadPage (url, options) method parameters


Parameter

Signification

url

Specifies the URL of the page you want to load into memory.

options.
pageContainer

jQuery class object indicating the element within which the new window will be displayed. Default $.mobile.pageContainer.

options.
data

The data option is an object or a string, corresponding to transmitted parameters.
- If using a string, it must be of the form name1=value1&name2=value2 etc., each name is the name of a parameter, and value the corresponding value encoded in UTF-8.
- If you use an object, jQuery Mobile itself encodes UTF-8 each value, and sends the server a string of the form name1=value1&name2=value2 etc.

options.
type

Method describing how to transmit parameters ("post" or "get"). The default is "get".

options.
reloadPage

If true, specifies to reload the window in the DOM, each viewing the page. Default false (the window is loaded into the DOM at the first display and is used as is).


Simulating the data-prefetch attribute using the $.mobile.loadPage ()


링크 내에 data-prefetch attribute 가 설정 되면 jQuery Mobile은 그 링크의 href attribute 에 해당하는 HTML 페이지를 백그라운드로 로딩합니다. 이 작업을 하기 위해 jQuery Mobile은 $.mobile.loadPage () method를 이용해서 메모리에 해당 윈도우를 저장하는 것이죠.


아래에 $.mobile.loadPage () method를 사용해서 윈도우를 preload 하는 예제를 보시겠습니다.



Preload a window with $.mobile.loadPage () method


<!DOCTYPE html>

<html> 

<head> 

  <meta name=viewport content="user-scalable=no,width=device-width" />

  <link rel=stylesheet href=jquery.mobile/jquery.mobile.css />

  <script src=jquery.js></script>

  <script src=jquery.mobile/jquery.mobile.js></script>

</head> 


<body> 


<div data-role=page id=home>

  <div data-role=header>

    <h1>Home</h1>

  </div>


  <div data-role=content>

    <p> Window content 1 </p>  

    <a href=# id=link1> Goto window 2 </a>

  </div>

</div>


</body>

</html>


<script>


$("#home").bind ("pagecreate", function ()

{

  $.mobile.loadPage ("index2.html");

});


$("#link1").bind ("click", function (event)

{

  $.mobile.changePage ("index2.html");

});


</script>



index2.html file containing the second window


<!DOCTYPE html>

<html> 

<head> 

  <meta http-equiv=Content-Type content=text/html;charset=iso-8859-1 />

  <link rel=stylesheet href=jquery.mobile/jquery.mobile.css />

  <script src=jquery.js></script>

  <script src=jquery.mobile/jquery.mobile.js></script>

</head> 


<body> 


<div data-role=page id=win2 data-add-back-btn=true data-dom-cache=true>

  <div data-role=header>

    <h1>Window 2</h1>

  </div>


  <div data-role=content>

    <p> Window content 2</p>

  </div>

</div>


</body>

</html>


convention181.html


convention182.html



두번째 윈도우는 index2.html 파일을 포함하게 됩니다. 일단 첫번째 윈도우가 생성되면 (pagecreate event) 곧바로 두번째 윈도우가 DOM tree 에 include 되겠죠.




일단 두번째 윈도우가 로드되면 유저가 링크를 클릭하면 $.mobile.changePage () method를 사용해서 두번째 윈도우를 display 할 겁니다.

이 때 유저는 jQuery Mobile의 waiting message (Loading) 를 보지 않아도 됩니다. 왜냐하면 그 페이지가 이미 DOM 에 있으니까요. 두번째 윈도우에서 data-dom-cache=true attribute를 세팅해서 DOM 내에 두번째 윈도우가 계속 머물러 있게 됩니다. 그러면 첫번째 윈도우에서 링크를 클릭하면 곧바로 두번째 윈도우가 display 되는 겁니다.


반응형


반응형

$.mogile.changePage(toPage,options) method



이전 글에서 HTML 페이지내에서 링크를 거는 간단한 방법을 보여드렸습니다. 이 링크는 두개의 윈도우간의 transition을 가능하게 하죠. 그 윈도우가 같은 HTML 페이지 안에 있던 아니면 다른 HTML 안에 있던 상관없습니다.


이 두개의 윈도우간 transition을 좀 manage 하고 싶은 경우에 어떻게 할까요? jQuery Mobile 은  이를 위해 $.mobile.changePage (toPage, options)를 제공하고 있습니다. 이름에서 알 수 있듯이 이것은 $.mobile object 상에 정의된 changePage () 메소드 입니다.



toPage parameter (required)는 여러분이 display 하기를 원하는 윈도우나 페이지를 말하는 겁니다. options parameter (optional)는 이 윈도우를 display 하기 위해 사용되는 옵션들을 가리키는 객체입니다.


$.mobile.changePage (toPage, options) method parameters


Parameter

Signification

toPage

Indicates the window or the URL of the page you want displayed.
- For a window, it is a jQuery class object (eg $("#win2") to display the window with this id). In this case, the window must already exist in the DOM.
- For a URL, it is a string (eg "index2.html"). In this case, the first window in the file is displayed.

options.
transition

One of the values slide, slideup, slidedown, pop, fade or flip, corresponding to the transition effect between the two windows (slide by default). See details below.

options.
reverse

If true, specifies to reverse the direction of the transition effect. Default false.

options.
changeHash

Indicates whether the URL in the address bar should be changed to reflect the URL of the new page or window displayed (if changeHash is true, default), or must retain the old value (if changeHash is false).

options.
pageContainer

jQuery class object indicating the element within which the new window will be displayed. Default $.mobile.pageContainer.

options.
data

The data option is an object or a string, corresponding to transmitted parameters.
- If using a string, it must be of the form name1=value1&name2=value2 ..., each name is the name of a parameter, and value the corresponding value encoded in UTF-8.
- If you use an object, jQuery Mobile itself encodes UTF-8 each value, and sends the server a string of the form name1=value1&name2=value2 etc.

options.
type

Method describing how to transmit parameters ("post" or "get"). The default is "get".

options.
reloadPage

If true, specifies to reload the window in the DOM, each viewing the page. Default false (the window is loaded into the DOM at the first display and is used as is). This option is only used if the argument toPage refers to a URL (the window is loaded by jQuery Mobile with Ajax).

options.
showLoadMsg

Boolean indicating to display the message saying that an HTML page is being loaded. The message is described in $.mobile.loadingMessage ("loading" by default).


Possible values of the data-transition attribute


data-transition

Signification

slide

Moving from one window to another by a horizontal movement from right to left. This is the default.

slideup

The second window appears at the bottom, gradually covering the first.

slidedown

The second window appears at the top, gradually covering the first.

pop

The second window is the center of the first, widening to cover it.

fade

The first window disappears by reducing its opacity (from 1 to 0), while the second appears through an increase in opacity (from 0 to 1).

flip

The second window appears with a rotation effect on a vertical axis, and by removing the first window.


링크를 클릭했을 때 $.mobile.changePage ()과  jQuery Mobile 이 만든 href attribute 사이에는 어떤 일이 일어 날까요? 어떤게 더 우선일까요? 


헛갈릴 필요가 없는게요 jQuery Mobile은 자바스크립트 코드에서 어떤 일이 발생할 때는 <a> link의 href attribute에 "#" value를 할당하라고 하거든요. 링크 내의 href="#"는 jQuery Mobile에게 일반적인 프로세스로 진행하지 않을거라고 얘기하는 거거든요. (그러니까 이럴 경우에는 자바스크립트 내에 있는 우리의 코드가 우선으로 적용되게 되는 겁니다.)


$.mobile.changePage (toPage, options) method를 사용한 아래 샘플들을 보세요.


Display a window in the same HTML page


하나의 HTML 페이지에 두개의 윈도우가 있다고 가정하죠. 첫번째 윈도우에서 두번째 윈도우로 움직이고 싶구요 이것을 $.mobile.changePage () method를 사용해서 링크를 관리할 겁니다. (링크의 href attribute를 사용하는 대신에요.)


Use the $. Mobile.changePage () method to display a window in the same HTML page


<!DOCTYPE html>

<html> 

<head> 

  <meta name=viewport content="user-scalable=no,width=device-width" />

  <link rel=stylesheet href=jquery.mobile/jquery.mobile.css />

  <script src=jquery.js></script>

  <script src=jquery.mobile/jquery.mobile.js></script>

</head> 


<body> 


<div data-role=page id=home>

  <div data-role=header>

    <h1>Home</h1>

  </div>


  <div data-role=content>

    <p> Window content 1 </p>  

    <a href=# id=link1> Goto window 2 </a>

  </div>

</div>


<div data-role=page id=win2 data-add-back-btn=true>

  <div data-role=header>

    <h1>Window 2</h1>

  </div>


  <div data-role=content>

    <p> Window content 2 </p>

  </div>

</div>


</body>

</html>


<script>


$("#link1").bind ("click", function (event)

{

  $.mobile.changePage ($("#win2"));

});


</script>



Window 2 에는 win2 라는 id 가 있습니다. 이것은 jQuery class 객체  $("#win2")로 다뤄질 수 있습니다. link attributes의 href="#" click event (instead of vclick)의 용도를 잘 생각해 보세요.  (이 이벤트는 link 에 position 돼 있어서 click 을 사용한 겁니다. 자세한 내용은 이전 글을 보세요.)






Display a window in another HTML page


이제는 두번째 윈도우가 다른 HTML 페이지에 있을 경우입니다. 두번째 윈도우를 보기위해 $.mobile.changePage () method를 사용합니다. 이 메소드의 첫번째 파라미터로 HTML 페이지의 URL 을 넣습니다.


Use the $.mobile.changePage () method to display a window in a new HTML page


<!DOCTYPE html>

<html> 

<head> 

  <meta name=viewport content="user-scalable=no,width=device-width" />

  <link rel=stylesheet href=jquery.mobile/jquery.mobile.css />

  <script src=jquery.js></script>

  <script src=jquery.mobile/jquery.mobile.js></script>

</head> 


<body> 


<div data-role=page id=home>

  <div data-role=header>

    <h1>Home</h1>

  </div>


  <div data-role=content>

    <p> Window content 1 </p>  

    <a href=# id=link1> Goto window 2 </a>

  </div>

</div>


<div data-role=page id=win2 data-add-back-btn=true>

  <div data-role=header>

    <h1>Window 2</h1>

  </div>


  <div data-role=content>

    <p> Window content 2 </p>

  </div>

</div>


</body>

</html>


<script>


$("#link1").bind ("click", function (event)

{

  $.mobile.changePage ("index2.html");

});


</script>



Index2.html file


<!DOCTYPE html>

<html> 

<head> 

  <meta http-equiv=Content-Type content=text/html;charset=iso-8859-1 />

  <link rel=stylesheet href=jquery.mobile/jquery.mobile.css />

  <script src=jquery.js></script>

  <script src=jquery.mobile/jquery.mobile.js></script>

</head> 


<body> 


<div data-role=page id=win2 data-add-back-btn=true>

  <div data-role=header>

    <h1>Window 2</h1>

  </div>


  <div data-role=content>

    <p> Window content 2</p>

  </div>

</div>


</body>

</html>



결과는 이전 예제와 거의 같습니다. 첫번째 링크에서는 약간 다를겁니다. 왜냐하면 처음에는 그 HTML 페이지가 로드되지 않았을 것이기 때문에요. 아마 속도가 느리면 두번째 윈도우가 열릴 때까지  Loading 이라는 메세지가 뜰 겁니다.


만약 index2.html file안에 여러개의 윈도우가 있다면? 이럴 경우에는 첫번째 윈도우면 DOM 에 적재 됩니다. 다른 윈도우들은 접근하지 못하는 상황이 됩니다.


Transmit data when displaying the window


만약에 첫번째 윈도우가 두번째 윈도우에 어떤 정보를 전달해야 될 경우를 가정해 보죠. 이것은 $.mobile.changePage () call의 data option을 사용하게 됩니다. 이렇게 되면 두번째 윈도우에 파라미터를 전달할 수가 있습니다.


Use the $.mobile.changePage () method to transmit information


<!DOCTYPE html>

<html> 

<head> 

  <meta name=viewport content="user-scalable=no,width=device-width" />

  <link rel=stylesheet href=jquery.mobile/jquery.mobile.css />

  <script src=jquery.js></script>

  <script src=jquery.mobile/jquery.mobile.js></script>

</head> 


<body> 


<div data-role=page id=home>

  <div data-role=header>

    <h1>Home</h1>

  </div>


  <div data-role=content>

    <p> Window content 1 </p>  

    <a href=# id=link1> Goto window 2 </a>

  </div>

</div>


</body>

</html>


<script>


$("#link1").bind ("click", function (event)

{

  $.mobile.changePage ("action.php", 

  {

    data : { fname : "Eric", lname : "Sarrion" }

  });

});


</script>


Display the new window (action.php file)


<?
  $fname = $_REQUEST["fname"];
  $lname = $_REQUEST["lname"];
  $fname= utf8_decode ($fname);
  $lname= utf8_decode ($lname);
  
  $html = "";
  $html .= "<div data-role=page data-add-back-btn=true>";
  $html .=   "<div data-role=header>";
  $html .=     "<h1>Window 2</h1>";
  $html .=   "</div>";
  $html .=   "<div data-role=content>";
  $html .=   "<p>Window content 2</p>";
  $html .=   "<p>First name : $fname</p>";
  $html .=   "<p>Last name : $lname</p>";
  $html .=   "</div>";
  $html .= "</div>";
  echo utf8_encode ($html);
?>



아래 이미지를 보시면 두번째 윈도우에 파라미터가 display 되고 있습니다.




Modify the transition to display the window


지금 까지는 두 윈도우간 이동시 디폴트 transition 만 사용했습니다. jQuery Mobile은 $.mobile.changePage () call에서 transition option을 사용해서 특정 transition을 설정할 수 있는 기능을 제공합니다.


이전 예제에 transition option을 추가 하겠습니다.


Use slideup to transition between the two windows


$.mobile.changePage ("action.php",

{

  data : { fname : "Eric", lname : "Sarrion" },

  transition : "slideup"

});




두번째 윈도우가 나타날 때 이전과는 다르게 나타날 겁니다.


Create a window and then dynamically display when a click occurs


HTML 안에 이미 만들어진 윈도우를 display 하는 대신에 링크를 클릭하면 $.mobile.changePage () method로 dynamic 하게 새로운 윈도우를 만들어서 display 하고 싶습니다.


Creating window dynamically


<!DOCTYPE html>

<html> 

<head> 

  <meta name=viewport content="user-scalable=no,width=device-width" />

  <link rel=stylesheet href=jquery.mobile/jquery.mobile.css />

  <script src=jquery.js></script>

  <script src=jquery.mobile/jquery.mobile.js></script>

</head> 


<body> 


<div data-role=page id=home>

  <div data-role=header>

    <h1>Home</h1>

  </div>


  <div data-role=content>

    <p> Window content 1 </p>  

    <a href=# id=link1> Goto window 2 </a>

  </div>

</div>


</body>

</html>


<script>


var html = "";

html += "<div data-role=page id=win2 data-add-back-btn=true>";

html +=   "<div data-role=header>";

html +=     "<h1>Window 2</h1>";

html +=   "</div>"

html +=   "<div data-role=content>";

html +=     "<p> Window content 2 </p>";

html +=   "</div>";

html += "</div>";


$(html).insertAfter ("#home");


$("#link1").bind ("click", function (event)

{

  $.mobile.changePage ($("#win2"));

});


</script>



이럴 경우에 insertAfter (selector) jQuery method를 사용해서 표현하시면 됩니다. (DOM 안에 첫번째 윈도우에 이어서 만들어진 윈도우가 삽입되겠죠. 그 다음에 링크를 누르면 그 새로운 윈도우가 display 되는 겁니다.)


반응형


반응형
Posted on . Written by


iOS6의 landscape/Game Center orientation issue 와 관련 코로나에서 많은 개선을 했다는 것을 말씀 드리게 되서 기쁩니다. 이 개선된 사항들은 Corona daily build 930에서 사용하실 수 있습니다.

처음 저희가 이 문제에 접근할 때 물리학자들이 자연의 forces들을 unify 하기 위한 접금 자세와 같은 자세였습니다. 최대한 일반적인 해결책을 찾으려고 애를 썼죠. 이 iOS6와 이슈와 관련한 해결책을 찾으면서 iOS5와 iOS 4.3 버전도 같이 동시에 테스트 해 가면서 일반적인 해결법을 찾으로겨 노력했습니다. 그래서 약간 stopgap 문제 같은게 생기기도 했었죠. ( #3 in last week’s FAQ)


개인적으로 그 stopgap 이 마음에 들지 않았습니다. 그래서 지난주에 팀에게 다른 방법을 찾아 보라고 부탁했고 좀 더 나은 방법을 찾아보자고 제안했습니다. 금요일 이전에 새롭게 전략을 만들었고 이대로 하면 훨씬 나아질 것이라고 생각했습니다.


이 새 전략은 거의 외과수술 수준의 작업을 해야 하는 작업입니다. 저희들이 무엇을 하는가 하면 이 workaround 가 오직 iOS 6 에서만 일어나도록 하는 겁니다. 이 현상은 iPhone 과 iPod Touch에서만 일어나거든요. -iPad에서는 이러한 현상이 일어나지 않습니다. -





저희들이 해야 할 일은 여러분의 info.plist에 special key 인 ‘CoronaUseIOS6LandscapeOnlyWorkaround’ 를 추가 하는 겁니다. 저희들이 이름을 조금 복잡하고 unfriendly하게  만들었는데요 애플이 다음버전에서는 이 버그를 해결하기를 바라는 마음에서 그렇게 지었습니다.


이 기능을 사용하시려면 build.settings 파일에  special ‘plist’ section을 추가하시면 됩니다.


settings =
{
orientation =
{
default = "landscapeRight",
supported =
{
"landscapeRight",
"landscapeLeft",
},
},
iphone =
{
plist =
{
CoronaUseIOS6LandscapeOnlyWorkaround = true,
},
}
}

 

(NOTE: 만약 이전의 workaround를 사용하신다면 여러분은 portrait 세팅을 remove 하셔야 합니다. 그리고 content key 도 remove 하셔야 됩니다. 그렇게 하지 않으시려면 그냥 위의 코드를 사용하시면 됩니다.)


이건 기본적으로 여러분 앱이 landscape 앱이라는 것을 코로나에 알려주는 겁니다. 그러면 이 special workaround를 activate 하고 싶다는 것을 얘기하는 것이죠. 이 workaround는 iOS 6 에서 돌아가는 iPhone 이나 iPod Touch 에서만 작동할 겁니다. iOS 5 에는 GameCenter bug 가 없습니다. 그러니까 거기서는 여러분의 landscape 앱이 제대로 잘 작동할 겁니다.


이제 마지막 문제가 있습니다. iOS 6 에서 돌아가는 iPad 의 photo picker 이슈 입니다. 만약에 여러분이 landscape 앱을 가지고 있다면 이 이슈가 발생할 겁니다. 이 부분은 다음 daily build 에서 해결책을 적용할 예정입니다.


반응형

Sprite Sheet 활용하기 (Advanced)

2012. 10. 12. 05:54 | Posted by 솔웅


반응형
Posted on . Written by


오늘의 게스트 Tutorial은 Omid Abourai 입니다. 인디 게임 개발자 이며 ArdenKid 라는 별명을 가지고 있습니다. 그는 2년여간 코로나 SDK 를 가지고 개발을 했으며 곧 그가 주도한 첫 게임인 "Balloon Bazooka"를 릴리즈 할 예정입니다. 그의 블로그는 www.ardentkid.com 입니다.  



Basic Sprite Sheets


만약 지난주의 를 animation tutorial 놓치셨다면 그것 부터 보세요. 이 튜토리얼은 지난주 튜토리얼의 basic sprite methods 의 연장선에 있는 겁니다.


Sprite sheet는 CPU 의 부담을 덜어준다는 면에서 아주 훌륭한 기술입니다. 그런데 그냥 vector animation 과 비교해서 단점이 있는 것도 사실입니다. 예를 들어 메모리를 많이 잡아먹는다든지 앱 크기가 커진다든지 하는 것들이요. (애플의 경우 20메가가 넘으면 wifi로 다운로드 받으라고 하는데 저희는 한 앱이 20메가를 넘지 않도록 작업을 하고 있습니다.)

그리고 예를 들어 한 캐릭터가 옷을 갈아입고 악세사리를 바꾸고 할 경우는 어떨까요? 우리의 캐릭터를 각 옷별로 또 악세사리별로 중복해서 만든다면 sprite sheet 를 쓰는 잇점이 줄어들겠죠. 이럴 경우 처음부터 부분 부분 별로 만들어서 활용한다면 적은 이미지 용량으로 훨씬 많은 효과를 낼 수 있을 겁니다.



이 튜토리얼은 그 이슈와 관련해서 가능한 해결법을 다룰 겁니다. 비밀은 코딩과 애니메이션 간에 좀 더 긴밀한 관계를 만드는 겁니다.

조만간 발매될 Balloon Bazooka 앱에서 사용했던 kid 관련 스프라이트 쉬트를 샘플로 시작해 보겠습니다.






이 sheet 는 코로나의 basic sprite animation에 맞게 디자인 됐습니다. 이미지를 로드한 다음 sequence들을 셋업하고 원하는 sequence들을 play 하는 방식이죠. 좀 더 진보된 테크닉을 원하신다면 아래 내용을 말씀해 드리고 싶습니다. 그런데 일단 우선은 이 basic methods가 익숙해지셔야 합니다.


Splitting the Animation Elements


우리는 화면에서 동시에 같은 sprite 표현을 사용하는 여러 kids 를 만들겁니다. 우리는 그들에게 다양한 옷과 살 색 등등을 적용할 겁니다. 다행히도 프로그램적으로 비쥬얼 property들을 tweening 함으로서 비슷한 애니메이션들을 만들 수 있습니다. 프로그램으로 scale, rotation, translation 등을 다야하게 적용할 수 있습니다. (우선 편의상 이번에는 팔은 가만히 있고 발만 움직이는 걸로 한번 만들어 보겠습니다.)


먼저 이 kid를 토막을 내겠습니다. color 별로 따로 만들고 나중에 이것들을 합칠께요. 이 작업을 하기 위한 새로운 image sheet 는 아래와 같을 겁니다.





이 new sheet 의 file size 가 225KB 에서 109KB로 줄어들었습니다. 그러면서도 다양하게 의상과 피부색을 조합할 수 있게 됐죠. 50%가 넘게 파일 사이즈를 줄였고 우리고 표현하고 싶은 다양한 kid 들을 표현할 수 있게 됐습니다.


이제 이 각각의 element들을 코드안에 individual sprite 로 정의 하겠습니다.


-- DECLARE CHARACTER SEQUENCES (MOST ARE JUST STATIC CHERRY-PICKED FRAMES!)
local sequenceData = {
  { name="beachboy_hat_yellow", frames={1} },
  { name="beachboy_body_dark", frames={34} },
  { name="beachboy_shorts_red", frames={42} },
  { name="beachboy_arm_dark", frames={ 5,12,15,19,23,26 }, loopDirection="bounce" },
  { name="beachboy_foot_dark", frames={47} }
}

-- CREATE A DISPLAY GROUP FOR THE CHARACTER
local beachboy = display.newGroup()

-- CREATE BODY PARTS AS SPRITES
local hat = display.newSprite( sheet, sequenceData )
      hat:setSequence( "beachboy_hat_yellow" )
local body = display.newSprite( sheet, sequenceData )
      body:setSequence( "beachboy_body_dark" )
local shorts = display.newSprite( sheet, sequenceData )
      shorts:setSequence( "beachboy_shorts_red" )
local rightArm = display.newSprite( sheet, sequenceData )
      rightArm:setSequence( "beachboy_arm_dark" )
local leftArm = display.newSprite( sheet, sequenceData )
      leftArm:setSequence( "beachboy_arm_dark" )
local rightFoot = display.newSprite( sheet, sequenceData )
      rightFoot:setSequence( "beachboy_foot_dark" )
local leftFoot = display.newSprite( sheet, sequenceData )
      leftFoot:setSequence( "beachboy_foot_dark" )

-- POSITION PARTS & ORIENT L/R SIDES WITH SCALING
shorts.x, shorts.y = 0, 15
leftArm.x, leftArm.y = -20, -18
leftArm.xScale = -1 --flip 'leftArm' horizontally
-- etc...

-- INSERT ELEMENTS INTO THE DISPLAY GROUP, ORDERED BOTTOM TO TOP
beachboy:insert(leftFoot)
beachboy:insert(rightFoot)
beachboy:insert(shorts)
-- etc...

-- STORE REFERENCES TO EACH ELEMENT
beachboy["feet"] = {leftFoot, rightFoot}
beachboy["shorts"] = shorts
beachboy["body"] = body
-- etc...



Animating the Elements


이 모든 element들이 제자리를 잡고 여러분의 캐릭터가 show up 하면 이제 그 캐릭터의 발 움직임을 줘서 애니메이션을 만들 수가 있습니다.


--WALK SEQUENCE
function beachboy:walk()

  local feet = self.feet

  --CLEAR EXISTING TRANSITIONS IF RUNNING
  if not ( self.currentTransitions ) then self.currentTransitions = {} end --add container table if not present
  local currTrans = self.currentTransitions
  local tot = #currTrans
  for i = tot,1,-1 do --loop backwards through transitions and clear each one
    if (currTrans[i]) then transition.cancel(currTrans[i]) ; currTrans[i] = nil end
  end

  local t = 500 --WALK CYCLE TIMING
  local dist = 160 --FOOT MOVEMENT DISTANCE
  local ease = easing.inOutQuad --FOR A NATURAL SWINGING MOTION

  --RECURSIVE ANIMATION FUNCTION
  local function anim()
    for i = 1,#feet do
      currTrans[i] = transition.to(feet[i], {y=dist, time=t, transition=ease, onComplete=function()
        currTrans[i] = transition.to(feet[i], {y=0, time=t, transition=ease, onComplete=function()
          anim()
        end})
      end})
    end
  end

  --START THE ANIMATION
  anim()

end


 

이제 우리만의 Runtime animation code 를 call 하고 있죠? 우리는 이 튜토리얼에서 계속 반복되는 transition들을 사용하고 있습니다. 여기서 팔도 발하고 같이 어울리게 애니메이션을 주고 싶다면 여기서 조금 더 작업을 해야 합니다. (이 부분은 따로 다루지는 않겠습니다.)

이제 같은 메모리로 여러 color들을 표현할 수 있게 됐습니다.


“Tinting” the Elements



여기서 각 파트별로 programatic tinting을 사용해서 color 까지도 코딩으로 넣을 수가 있습니다. 이것은 코로나의 setFillColor() method를 사용하면 됩니다. 우선 모든 부분을 흰색으로 만듭니다. 아래 샘플 이미지가 있습니다.







아래에 코딩으로 어떻게 sprite object들에 tint를 하는지에 대한 예제가 있습니다.

-- ACCESS EACH ELEMENT FROM THE CONTAINER TABLE BY INDEX NAME
beachboy.hat:setFillColor(252, 206, 0)
beachboy.body:setFillColor(126, 79, 33)
beachboy.shorts:setFillColor(220, 0, 0)
beachboy.feet[1]:setFillColor(126, 79, 33) --access 'leftFoot' by position within 'feet' table
beachboy.feet[2]:setFillColor(126, 79, 33) --access 'rightFoot' by position within 'feet' table
-- etc...

  

이제 원하는 색을 넣을 수도 있고 랜덤하게 색을 넣을 수도 있습니다. 여러분 마음대로 하시면 됩니다. 단지 다른 animation sequence 될 때마다 색을 넣어야 하는 작업을 해야 되겠죠. 그리고 캐릭터가 움직이면서 좀 더 CPU 를 잡아 먹을 겁니다. 그리고 한 객체에 한가지 색만 넣을 수 있겠죠. 줄무늬 옷이나 뭐 그런거는 좀 힘듭니다. 흰색과 회색을 사용해서 조금 sprite 한 느낌은 줄 수 있겠죠.


In Summary



해 보니까 이 방법은 요즘 대부분의 모바일 디바이스에서 사용하는 예외적인 방법이더라구요. 이 방법을 사용하면 texture memory를 훨씬 줄일 수 있습니다. 우리는 이 beachboy 캐릭터의 용량을 225 KB에서 109 KB로 줄였었습니다. 그리고 마지막 방법까지 하면 12KB 로 95%나 줄일 수 있었습니다.


그리고 또 다른 효과는 각 파트별로 다른 애니메이션 효과를 줄 수 있게 된 겁니다. 팔 하나만 움직이게 할 수도 있고 두 팔을 다 움직이게 할 수도 있구요 또 더 나아가서는 펀치나 킥하는 장면도 넣을 수 있겠죠.


그리고 각 character element 별로 tint/color 를 할 수가 있어서요 아주 다양한 캐릭터를 만들어 낼 수 있게 됐습니다.

이렇게 dynamically-optimized sprite sheet 를 사용하면 아주 다양하고 훌륭한 효과를 낼 수가 있습니다. 그리고 여러분의 코딩에 따라서 한정된 이미지로 개성있는 캐릭터를 계속 만들어 낼 수 있구요.


반응형


반응형
Posted on . Written by


화요일의 Tutorial이 다시 돌아왔습니다. 오늘의 튜토리얼은

Brent Sorrentino 가 작성한 건데요. Northern Colorado 에서 Corona Ambassador 로 활동하고 있습니다. Brent는 2년여 코로나 커뮤니팅서 활발히 활동하고 있습니다. 그는 프리랜서 여행사진작가이고 코로나 개발자이며 그래픽 디자이너 입니다. 그만의 앱을 개발하기 위해 코로나를 사용하고 있습니다. 또한 정기적으로 포럼에서 여러사람에게 도움을 주고 있고 여러 이슈들을 해결해 주고 있습니다 그의 웹사이트를 보시려면 여기를 클릭하세요.



오늘의 튜토리얼은 어떻게 animated sprite를 implement 할 것인가와 그와 관련된 API 들에 관한 겁니다. 아마 이전 튜토리얼에서 다룬 내용도 있을 겁니다. 올해 초 현재의 스프라이트 시스템이 소개 된 이후 몇가지 주요 개선사항이 있었습니다. 그리고 많은 개발자들이 아직도 코로나에서 어떻게 sprite를 implement 하는지에 대해 혼동을 하고 있습니다. 이 sprite를 사용하는 방법에는 두가지가 있습니다.


  1. The old (and now depreciated) sprite.* library.
  2. The current sprite APIs that work cohesively with image sheets and the display.* library.


이렇게 코로나의 스프라이트 라이브러리가 변화를 보였는데요. 그 중에서 현재 버전을 사용할 것을 강력히 추천합니다. 현재 방법을 사용하지 않았거나 코로나에서 스프라이트를 한번도 사용해 보지 않았다면 이 튜토리얼이 많이 도움이 될 겁니다.


코로나의 basic animation에 이미 익숙하신 분이라면 이 튜토리얼이 sprite event listeners에 대한 모든 정보를 제공하고 어떻게 implement 할 것인지를 가이드 해 드릴 겁니다.


Tutorial Contents

  1. Configuring a Basic Image Sheet or “Sprite Sheet”
  2. Defining Animation Sequences
  3. Declaring a Sprite Object
  4. Sprite Control Methods — managing playback and sequences
  5. Sprite Properties — accessing and setting various sprite properties
  6. Sprite Event Listeners — detecting sequence events such as “ended”, “loop”, and “next”
  7. Frame Trimming Support


Configuring a Basic Image Sheet or “Sprite Sheet”



정확히 image sheet 이 뭘까요? 한번 상상해 보세요. 여러분들의 animated object 들을 위해 각각 의 프레임들을 그린 한장의 종이가 있다고요. 코로나에서 다른 전문적인 용어로는 texture atlas, image map, or sprite sheet 라고 합니다. 이걸 그냥 간단히 image sheet라고 부를께요. 이것은 static 이든 animated object 이든지 상관없이 활용할 수 있습니다.

graphics.newImageSheet() API에 대한 자세한 사용법과 샘플들은 여기에 있습니다. 이 튜토리얼에서는  그중에서 어떻게 animated sprite를 위해 image sheets를 사용할지에 대해 다루겠습니다.



아래 그림은 running cat 을 표현하는 샘플 image sheet 입니다. 이 튜토리얼을 공부하면서 이 이미지를 사용하시려면 여기에 hi-resolution version 이미지가 있습니다. 이 sheet 에는 8개의 프레임이 순서대로 있습니다. 디폴트로 애니메이션은 top-left frame 부터 시작해서 오른쪽으로 진행하죠. 오른쪽 끝까지 가면 그 다음 줄로 갑니다. 그리고 전체 프레임을 다 돌면 중지합니다.






코로나에서 이 image sheet 을 어떻게 다루는지 보겠습니다. 우선 uniform-sized frame 인 basic image sheet 를 위해 indexed table을 setup 합니다.

local sheetData = {
  width = 512, --the width of each frame
  height = 256, --the height of each frame
  numFrames = 8, --the total number of frames on the sheet
  sheetContentWidth = 1024, --the total width of the image sheet (see note below)

  sheetContentHeight = 1024 --the total height of the image sheet (see note below)

}


IMPORTANT:  sheetContentWidthsheetContentHeightoverall 1:1 dimensions of the image sheet (전체 가로 세로)를 나타냅니다.1:1 content scale 은 여러분이 앱의 config.lua file에 set up 한 내용을 기준으로 합니다.  이렇게 하면 다른 디바이스별로 다른 이미지를 사용할 수 있도록 합니다. 예를 들어 @1 sheet는 original iPad 에 맞는 이미지를 사용하고 high-resolution @2 sheet 는 Retina iPad 에 맞는 이미지를 사용할 수 있습니다. 그러니까 1:1 sheet이 1024×1024 pixels 이라면 2:1 sheet 는 2048×2048 인 이미지가 되는거죠. image sheet setup에는 항상 1:1 dimensions를 사용하세요. config.lua file 세팅이 정확하다면 그 다음부터는 코로나가 알아서 처리할 겁니다.



이제 실제 image sheet 를 정의하세요. 괄호 안에 image file 과 위에 정의한 data table을 넣어주시면 됩니다. 아래처럼 파일이름만 넣으면 그 이미지 파일을 여러분 프로젝트 디렉토리의 root 에서 찾을 겁니다. 


local mySheet = graphics.newImageSheet( "runningcat.png", sheetData )


Defining Animation Sequences


여러분은 두가지 방법으로 animated frame 순서를 정하실 수 있습니다.

  • consecutively using a starting frame index and frame count
  • non-consecutively using a specific order of frames


이 두 가지 방법으로 코로나에서 sprite system 을 아주 flexible 하게 사용하실 수 있습니다. 하나의 image sheet 으로 여러 animation sequences를 사용해서 활용할 수가 있죠. 두 경우 모두 sub-table에서 comma-separated array 로 sequences를 define 하시면 됩니다.


Consecutive frames


local sequenceData = {
  { name = "normalRun", --name of animation sequence

    start = 1, --starting frame index

    count = 8, --total number of frames to animate consecutively before stopping or looping

    time = 800, --optional, in milliseconds; if not supplied, the sprite is frame-based

    loopCount = 0, --optional. 0 (default) repeats forever; a positive integer specifies the number of loops

    loopDirection = "forward" --optional, either "forward" (default) or "bounce" which will play forward then backwards through the sequence of frames

  } --if defining more sequences, place a comma here and proceed to the next sequence sub-table


}



Non-consecutive frames


local
sequenceData = {
  { name = "fastRun",
    frames = { 1,2,4,5,6,7 }, --specific order of frame indexes from the image sheet

    time = 250,
    loopCount = 0
  } --if defining more sequences, place a comma here and proceed to the next sequence sub-table

}

Mixed sequences (both consecutive and non-consecutive sequences)


local
sequenceData = {
{ name="normalRun", start=1, count=8, time=800 },
  { name="fastRun", frames={ 1,2,4,5,6,7 }, time=250, loopCount=0 }

}

 

Declaring the Sprite Object


현재의 sprite method를 사용하려면 display.newSprite() API 로 sprite를 선언하셔야 합니다. 신택스는 간단합니다.


display.newSprite( [parent,] imageSheet, sequenceData )
  • parent = The parent display group in which to insert the sprite (optional).
  • imageSheet = The image sheet which the sprite should utilize.
  • sequenceData = The array of animation sequences which you set up. The sprite will default to the first sequence in the array unless you specify otherwise (see Sprite Control Methods below).


이 튜토리얼에 나온 예제로 한다면 sprite 선언은 아래와 같을 겁니다.

local animation = display.newSprite( mySheet, sequenceData )


이제 이 sprite 는 display object가 됐습니다. 그냥 일반 static image, vector objects 같은 것들과 같게 됐죠. 이제 이 sprite 는 움직일 수도 있고 manipulated 될 수도 있고 physics body 를 입힐 수도 있고... 등등을 할 수 있습니다. 이 sprite object 를 remove 시키려면object:removeSelf()display.remove( object ) 를 사용하시면 됩니다. remove 한 다음에 nil로 세팅하는 것을 잊지 마세요.


Sprite Control Methods


이 스프라이트 시스템은 4가지 주요 control methods를 제공합니다. 여러분들은 이것으로 스프라이트의 playback과 sequence 를 control 하실 수 있습니다.


  • animation:play()
    Start the animation playing. Animations do not begin playing when you create them — you must start each animation manually using this command.
  • animation:pause()
    Pauses the animation. There is no “stop” control method; instead, pause the animation using this method.
  • animation:setFrame( frame )


    Immediately set or skip to the indicated frame index. If you want to “stop and reset” an animation sometime after you have started playing it, use the :pause() and :setFrame( frame ) commands consecutively, setting the frame back to the beginning of the sequence.
  • animation:setSequence( sequence )


    Set the sprite to a specific sequence that you declared in your sequence array. For example, if you want to change your cat animation from “normalRun” to “fastRun”, you would use animation:setSequence( “fastRun” ) and use animation:play() to begin playing it, since the animation will not play automatically after you change the sequence.


Putting It Together


전체 애니메이션을 함께 넣어보죠. 달리는 고양이를 normalRun 과 fastRun 두가지로 셋업할 겁니다. 아래 예제가 있습니다.

local sheetData = { width=512, height=256, numFrames=8,

sheetContentWidth=1024, sheetContentHeight=1024 }

local mySheet = graphics.newImageSheet( "runningcat.png", sheetData )

local sequenceData = {
  { name = "normalRun", start=1, count=8, time=800 },
  { name = "fastRun", frames={ 1,2,4,5,6,7 }, time=250 }
}



local animation = display.newSprite( mySheet, sequenceData )
animation.x = display.contentWidth/2 --center the sprite horizontally
animation.y = display.contentHeight/2 --center the sprite vertically

animation:play()





코로나 시뮬레이터로 테스트 해 보세요. 저 고양이가 화면 중앙에 나올겁니다. 그리고 normalRun sequence로 animating 되겠죠. (sequence를 따로 선언하지 않으면 위에 얘기했던 디폴트 순수대로 진행합니다.)
테스트를 위해 animation:play()앞에 animation:setSequence( “fastRun” )를 넣어 보세요.


Sprite Properties


Corona provides several properties which can yield information about existing sprites. You can even modify the timeScale (relative speed) of a particular sprite. These properties are as follows:

코로나에는 현재의 스프라이트에 적용할 수 있는 몇가지 프로퍼티들을 제공합니다. 특정 스프라이트의 timeScale (relative speed)를 modify 할 수도 있습니다. 아래 프로퍼티들을


  • object.frame
    A read-only property that represents the currently shown frame index of the loaded sequence. This does not set the frame — use the :setFrame() command to explicitly set an animation frame.
  • object.isPlaying
    Returns true if the animation is currently playing; false if it is not.
  • object.numFrames
    A read-only property that represents the number of frames in currently loaded sequence.
  • object.sequence
    A read-only property that returns the name of the currently playing sequence.
  • object.timeScale
    Gets or sets the scale to be applied to the animation time. This is used to control a sprite’s animation speed dynamically. For example, a time scale of 1.0 (default) runs the animation at normal speed. A time scale of 2.0 runs the animation twice as fast. A time scale of 0.5 runs the animation at half speed. The maximum allowed value is 20.0 and the minimum allowed value is 0.05. The value supports up to 2 decimal places.


Sprite Event Listeners


이제 basic sprite 선언과 두개의 sequences (“normalRun” , “fastRun”) 가 생겼습니다. 

이제 sprite event listener를 살펴보죠. 그리고 그것을 어떻게 implement 하는지에 대해 알아보겠습니다. sprite event listener 의 정의는 'sprite의 activity를 "listens" 하고 그 정보를 listener function에 전달하는 것' 입니다.


예를 들어 여러분의 달리는 고양이를 "normalRun" sequence로 4번(4 cycles)를 돌게 한 다음에 "fastRun" sequence로 바꿀 수 있습니다. 이것은 standard timer 로 표현하기에는 불가능한 효과죠. 그래서  그 대신에 sprite event listener를 implement 하는 겁니다.



예제를 보기 전에 sprite에서 사용 가능한 5가지 를 event phases보겠습니다.

이 phases 들은 최근의 코로나 Public Release 인 (2012.894) 버전 이후부터 사용하실 수 있습니다.


  • began = The sprite has started playing.
  • ended = The sprite has finished playing.
  • bounce = The sprite has bounced from forward to backward while playing.
  • loop = The sprite has looped to the beginning of the sequence.
  • next = The sprite has played a subsequent frame that’s not one of the above phases.


이 phases 를 어떻게 listen 할지에 대해 다루겠습니다. 근데 우선 달리는 고양이에 loopCount = 4를 추가해서 normalRun 의 sequence를 바꾸고 시작하도록 하죠. 이렇게 하면 4번의 loop가 끝나면 ended phase를 받을 수 있도록 해 줍니다.


local sequenceData = {
  { name = "normalRun", start=1, count=8, time=800, loopCount=4 }, --add loopCount=4

  { name = "fastRun", frames={ 1,2,4,5,6,7 }, time=250 }
}

 

Now, let’s write the listener function and add the actual event listener to the running cat. You can place this at the end of your sample code, after the sequences are declared and the sprite object placed on the screen.

이제 listener function을 만들어 봅시다. 그리고 달리는 고양이에 실제 event listener를 달아보죠. 아래 내용을 위 샘플 코드의 마지막 부분에 sequence 가 선언된 다음에 추가하시면 됩니다.


local function mySpriteListener( event )
if ( event.phase == "ended" ) then
    local thisSprite = event.target --"event.target" references the sprite
    thisSprite:setSequence( "fastRun" ) --switch to "fastRun" sequence
    thisSprite:play() --play the new sequence; it won't play automatically!
  end

end

animation:addEventListener( "sprite", mySpriteListener ) --add a sprite listener to your sprite

 
sprite listener 가 이제 모든 phases를 listener 함수에 전달하게 됩니다. if-then 구문을 사용해서 이 phases를 사용하는 것은 개발자가 코딩해야 할 부분입니다. 특정 시점에 우리는 ended phase를 listen 하게 됩니다. sequence의 loopCount parameter에 의해 4번의 루프가 끝나면 이 ended phase가 발생하도록 했죠. 이 ended phase 를 받으면 이 cat animation을 fastRun sequence로 바꾸고 play 하게 됩니다.

하나의 sprite listener를 모든 sprite 에 대해 사용하실 수 있습니다. listener 함수에서 event.target을 사용해서 원하는 스프라이트를 catch 해서 사용하실 수 있는겁니다.


Frame Trimming Supported



최근의 Public Build (2012.894) 에서는 frame trimming 도 지원합니다. 3rd-party sprite utilities 인 SpriteLoqTexturePacker 같은 곳에서 이 기능을 제공하고 있습니다. 그리고 코로나에서도 이 어플리케이션과 호환성 있게 이 기능을 사용할 수 있도록 하고 있습니다.

frame trimming 예제는 current sprite methods 를 사용하고 있는데 이것은 여러분 시스템의 코로나 어플리케이션에 있는 “HorseAnimation” sample project에서 확인 하실 수 있습니다.


CoronaSDK → SampleCode → Sprites → HorseAnimation



이와 관련해서는 다음 튜토리얼에서 다뤄지게 될 것 같습니다. 그동안 여러분은 샘플 프로젝트를 보셔도 되고 여기에서 imageSheet documentation를 보시면 많은 도움이 되실 겁니다.


In Summary


이 튜토리얼은 current sprite methods 의 대부분을 다뤘습니다. 여기에 basic image sheets, animation sequences 정의하기, sprite playback 다루기, 다양한 sprite property들 다루기 그리고 sprite event listener 사용하기 등을 추가로 다루고 있습니다. 모쪼록 이 튜토리얼이 개발자 여러분에게 스프라이트를 이용해서 앱을 만드는데 도움을 드릴 수 있기를 바라겠습니다. 특히 이전 버전의 sprite library 를 사용하시던 분들에게 새로 바뀐 sprite library 를 사용하시는데 도움이 되기를 바라구요.


반응형